PNG  IHDR;IDATxܻn0K )(pA 7LeG{ §㻢|ذaÆ 6lذaÆ 6lذaÆ 6lom$^yذag5bÆ 6lذaÆ 6lذa{ 6lذaÆ `}HFkm,mӪôô! x|'ܢ˟;E:9&ᶒ}{v]n&6 h_tڠ͵-ҫZ;Z$.Pkž)!o>}leQfJTu іچ\X=8Rن4`Vwl>nG^is"ms$ui?wbs[m6K4O.4%/bC%t Mז -lG6mrz2s%9s@-k9=)kB5\+͂Zsٲ Rn~GRC wIcIn7jJhۛNCS|j08yiHKֶۛkɈ+;SzL/F*\Ԕ#"5m2[S=gnaPeғL lذaÆ 6l^ḵaÆ 6lذaÆ 6lذa; _ذaÆ 6lذaÆ 6lذaÆ RIENDB` #!/usr/bin/env php R*vendor/autoload.phpyY%vendor/composer/autoload_classmap.php|yY|>Ҷ"vendor/composer/autoload_files.phpyY> e'vendor/composer/autoload_namespaces.phpyYJö!vendor/composer/autoload_psr4.phpyY\!vendor/composer/autoload_real.phpn yYn kȔH#vendor/composer/autoload_static.php:yY:_"ٶvendor/composer/ClassLoader.php4yY4Q?vendor/consolidation/annotated-command/src/AnnotatedCommand.php:yY:I`Fvendor/consolidation/annotated-command/src/AnnotatedCommandFactory.php.yY.=vendor/consolidation/annotated-command/src/AnnotationData.phpuyYu9}Fvendor/consolidation/annotated-command/src/CommandCreationListener.phpyY[Ovendor/consolidation/annotated-command/src/CommandCreationListenerInterface.phpyY"\4:vendor/consolidation/annotated-command/src/CommandData.php yY Ɗ;vendor/consolidation/annotated-command/src/CommandError.phpPyYP9Cvendor/consolidation/annotated-command/src/CommandFileDiscovery.phpZ/yYZ/o|Jvendor/consolidation/annotated-command/src/CommandInfoAltererInterface.phpyYэ?vendor/consolidation/annotated-command/src/CommandProcessor.php&yY&MOvendor/consolidation/annotated-command/src/Events/CustomEventAwareInterface.php yY %CtKvendor/consolidation/annotated-command/src/Events/CustomEventAwareTrait.php\yY\6s'@vendor/consolidation/annotated-command/src/ExitCodeInterface.php yY +?vendor/consolidation/annotated-command/src/Help/HelpCommand.php_yY_@vendor/consolidation/annotated-command/src/Help/HelpDocument.phpyYR+쉶Evendor/consolidation/annotated-command/src/Help/HelpDocumentAlter.phpyYU֑Ivendor/consolidation/annotated-command/src/Hooks/AlterResultInterface.php#yY#~uKvendor/consolidation/annotated-command/src/Hooks/ExtractOutputInterface.phphyYhȉ@vendor/consolidation/annotated-command/src/Hooks/HookManager.php#UyY#UDoLvendor/consolidation/annotated-command/src/Hooks/InitializeHookInterface.phpyY VJHvendor/consolidation/annotated-command/src/Hooks/InteractorInterface.php:yY:ÍHvendor/consolidation/annotated-command/src/Hooks/OptionHookInterface.phpyYkKvendor/consolidation/annotated-command/src/Hooks/ProcessResultInterface.phpyY2UnNvendor/consolidation/annotated-command/src/Hooks/StatusDeterminerInterface.phpyYx<Gvendor/consolidation/annotated-command/src/Hooks/ValidatorInterface.php#yY#wPOvendor/consolidation/annotated-command/src/Options/AlterOptionsCommandEvent.php yY *5Xvendor/consolidation/annotated-command/src/Options/AutomaticOptionsProviderInterface.phpmyYmIfGvendor/consolidation/annotated-command/src/Options/PrepareFormatter.phpyYԍRqQvendor/consolidation/annotated-command/src/Options/PrepareTerminalWidthOption.phpjyYjQHBvendor/consolidation/annotated-command/src/OutputDataInterface.php?yY?Avendor/consolidation/annotated-command/src/Parser/CommandInfo.php@yY@<Nvendor/consolidation/annotated-command/src/Parser/DefaultsWithDescriptions.phpyYiǩ\vendor/consolidation/annotated-command/src/Parser/Internal/AbstractCommandDocBlockParser.phpg yYg \WUvendor/consolidation/annotated-command/src/Parser/Internal/CommandDocBlockParser2.phpmyYm;Uvendor/consolidation/annotated-command/src/Parser/Internal/CommandDocBlockParser3.php yY r[vendor/consolidation/annotated-command/src/Parser/Internal/CommandDocBlockParserFactory.phpyYp'ʠTvendor/consolidation/output-formatters/src/Exception/AbstractDataFormatException.phpyYWPRvendor/consolidation/output-formatters/src/Exception/IncompatibleDataException.phpyYOvendor/consolidation/output-formatters/src/Exception/InvalidFormatException.php9yY9Nvendor/consolidation/output-formatters/src/Exception/UnknownFieldException.phpPyYPYOvendor/consolidation/output-formatters/src/Exception/UnknownFormatException.phpVyYVfӖ?vendor/consolidation/output-formatters/src/FormatterManager.php=yY=6Fvendor/consolidation/output-formatters/src/Formatters/CsvFormatter.php{yY{5`Lvendor/consolidation/output-formatters/src/Formatters/FormatterInterface.phpyYJ?^Gvendor/consolidation/output-formatters/src/Formatters/JsonFormatter.phpyYxƻGvendor/consolidation/output-formatters/src/Formatters/ListFormatter.php&yY&YiɶIvendor/consolidation/output-formatters/src/Formatters/PrintRFormatter.phpyY=k>Mvendor/consolidation/output-formatters/src/Formatters/RenderDataInterface.php>yY>Tz۶Nvendor/consolidation/output-formatters/src/Formatters/RenderTableDataTrait.phpyY.՗Kvendor/consolidation/output-formatters/src/Formatters/SectionsFormatter.php yY vނLvendor/consolidation/output-formatters/src/Formatters/SerializeFormatter.phpyYIvendor/consolidation/output-formatters/src/Formatters/StringFormatter.php yY uL|Hvendor/consolidation/output-formatters/src/Formatters/TableFormatter.phpyY:j1Fvendor/consolidation/output-formatters/src/Formatters/TsvFormatter.phpyYĖLvendor/consolidation/output-formatters/src/Formatters/VarExportFormatter.phpyY֋Fvendor/consolidation/output-formatters/src/Formatters/XmlFormatter.php, yY, d rGvendor/consolidation/output-formatters/src/Formatters/YamlFormatter.phpCyYCGGvendor/consolidation/output-formatters/src/Options/FormatterOptions.php)yY)kkOvendor/consolidation/output-formatters/src/Options/OverrideOptionsInterface.phpyYOktTvendor/consolidation/output-formatters/src/StructuredData/AbstractStructuredList.php yY Mvendor/consolidation/output-formatters/src/StructuredData/AssociativeList.phpyY/Nvendor/consolidation/output-formatters/src/StructuredData/CallableRenderer.phpJyYJz(Jvendor/consolidation/output-formatters/src/StructuredData/HelpDocument.phpEyYE(T/Ovendor/consolidation/output-formatters/src/StructuredData/ListDataInterface.phpyY'1"Svendor/consolidation/output-formatters/src/StructuredData/OriginalDataInterface.phpyYaJvendor/consolidation/output-formatters/src/StructuredData/PropertyList.phpyY2,ֶ[vendor/consolidation/output-formatters/src/StructuredData/RenderCellCollectionInterface.phpyY;۶Wvendor/consolidation/output-formatters/src/StructuredData/RenderCellCollectionTrait.phpkyYk]öQvendor/consolidation/output-formatters/src/StructuredData/RenderCellInterface.phpyYp<}Rvendor/consolidation/output-formatters/src/StructuredData/RestructureInterface.phpyYc{Jvendor/consolidation/output-formatters/src/StructuredData/RowsOfFields.phpyYD)_Pvendor/consolidation/output-formatters/src/StructuredData/TableDataInterface.php"yY"nF8Rvendor/consolidation/output-formatters/src/StructuredData/Xml/DomDataInterface.phpyYֲKvendor/consolidation/output-formatters/src/StructuredData/Xml/XmlSchema.phpCyYC7:Tvendor/consolidation/output-formatters/src/StructuredData/Xml/XmlSchemaInterface.phpyYM}Svendor/consolidation/output-formatters/src/Transformations/DomToArraySimplifier.phpayYaeS[vendor/consolidation/output-formatters/src/Transformations/OverrideRestructureInterface.phpyY ^vendor/consolidation/output-formatters/src/Transformations/PropertyListTableTransformation.phpyYcMvendor/consolidation/output-formatters/src/Transformations/PropertyParser.phpyY٣sLvendor/consolidation/output-formatters/src/Transformations/ReorderFields.php|yY|ׄWvendor/consolidation/output-formatters/src/Transformations/SimplifyToArrayInterface.phpyY2yRvendor/consolidation/output-formatters/src/Transformations/TableTransformation.php yY HJvendor/consolidation/output-formatters/src/Transformations/WordWrapper.php yY 1Kvendor/consolidation/output-formatters/src/Validate/ValidationInterface.phpyYUOvendor/consolidation/output-formatters/src/Validate/ValidDataTypesInterface.phpjyYj 9PKvendor/consolidation/output-formatters/src/Validate/ValidDataTypesTrait.phpyY"+vendor/dnoegel/php-xdg-base-dir/src/Xdg.phpn yYn ̑wWvendor/doctrine/instantiator/src/Doctrine/Instantiator/Exception/ExceptionInterface.phpyY.ö]vendor/doctrine/instantiator/src/Doctrine/Instantiator/Exception/InvalidArgumentException.phpyYh7I]vendor/doctrine/instantiator/src/Doctrine/Instantiator/Exception/UnexpectedValueException.php yY " Gvendor/doctrine/instantiator/src/Doctrine/Instantiator/Instantiator.php yY &Pvendor/doctrine/instantiator/src/Doctrine/Instantiator/InstantiatorInterface.php~yY~:2vendor/jakub-onderka/php-console-color/example.phpyYșiXvendor/jakub-onderka/php-console-color/src/JakubOnderka/PhpConsoleColor/ConsoleColor.php+yY+Wpyavendor/jakub-onderka/php-console-color/src/JakubOnderka/PhpConsoleColor/InvalidStyleException.phpyY zAvendor/jakub-onderka/php-console-highlighter/examples/snippet.php,yY,7d߇Dvendor/jakub-onderka/php-console-highlighter/examples/whole_file.php'yY'bQvendor/jakub-onderka/php-console-highlighter/examples/whole_file_line_numbers.php6yY6=@cvendor/jakub-onderka/php-console-highlighter/src/JakubOnderka/PhpConsoleHighlighter/Highlighter.phpyY[ +vendor/nikic/php-parser/grammar/analyze.phpq yYq ]ὶ2vendor/nikic/php-parser/grammar/rebuildParsers.php!yY!j)vendor/nikic/php-parser/lib/bootstrap.phpyY]4vendor/nikic/php-parser/lib/PhpParser/Autoloader.phpyY+8vendor/nikic/php-parser/lib/PhpParser/Builder/Class_.php yY `=vendor/nikic/php-parser/lib/PhpParser/Builder/Declaration.phpyY3a;vendor/nikic/php-parser/lib/PhpParser/Builder/Function_.phpLyYL]%o>vendor/nikic/php-parser/lib/PhpParser/Builder/FunctionLike.phplyYl <vendor/nikic/php-parser/lib/PhpParser/Builder/Interface_.phpyYK j8vendor/nikic/php-parser/lib/PhpParser/Builder/Method.php yY <vendor/nikic/php-parser/lib/PhpParser/Builder/Namespace_.phpyY@/7vendor/nikic/php-parser/lib/PhpParser/Builder/Param.phpyY+`:vendor/nikic/php-parser/lib/PhpParser/Builder/Property.php yY Y8vendor/nikic/php-parser/lib/PhpParser/Builder/Trait_.phpyYo6vendor/nikic/php-parser/lib/PhpParser/Builder/Use_.phpyY-;1vendor/nikic/php-parser/lib/PhpParser/Builder.phpyYD\9vendor/nikic/php-parser/lib/PhpParser/BuilderAbstract.phpMyYMo#8vendor/nikic/php-parser/lib/PhpParser/BuilderFactory.phpt yYt Ӂ5vendor/nikic/php-parser/lib/PhpParser/Comment/Doc.phpMyYMMӶ1vendor/nikic/php-parser/lib/PhpParser/Comment.phpyY܋P/vendor/nikic/php-parser/lib/PhpParser/Error.phpyY4sB9vendor/nikic/php-parser/lib/PhpParser/Lexer/Emulative.phpyY1@/vendor/nikic/php-parser/lib/PhpParser/Lexer.php)yY) 2vendor/nikic/php-parser/lib/PhpParser/Node/Arg.phpyYf@~5vendor/nikic/php-parser/lib/PhpParser/Node/Const_.phpyYܲ:vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Array_.phpyYAvendor/nikic/php-parser/lib/PhpParser/Node/Expr/ArrayDimFetch.phpyY *=vendor/nikic/php-parser/lib/PhpParser/Node/Expr/ArrayItem.phphyYhC¶:vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Assign.phpyYGvendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/BitwiseAnd.phpxyYx*Fvendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/BitwiseOr.phpwyYw!Gvendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/BitwiseXor.phpxyYxu0FCvendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/Concat.phptyYt&@vendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/Div.phpqyYqxEֶBvendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/Minus.phpsyYs@vendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/Mod.phpqyYqZI@vendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/Mul.phpqyYqyAvendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/Plus.phpryYr[Wr @vendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/Pow.phpqyYq:Fvendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/ShiftLeft.phpwyYw!Gvendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp/ShiftRight.phpxyYx2ֶ<vendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignOp.phpyY%=vendor/nikic/php-parser/lib/PhpParser/Node/Expr/AssignRef.phpyY&%Gvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/BitwiseAnd.phpxyYxH遶Fvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/BitwiseOr.phpwyYwp׶Gvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/BitwiseXor.phpxyYxGvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/BooleanAnd.phpxyYxFvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/BooleanOr.phpwyYw@Evendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/Coalesce.phpwyYwW|]Cvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/Concat.phptyYtG~@vendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/Div.phpqyYqpBvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/Equal.phpsyYs€VDvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/Greater.phpuyYu>mKvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/GreaterOrEqual.php|yY|قFvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/Identical.phpwyYw2Gvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/LogicalAnd.phpxyYxJ@Fvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BinaryOp/LogicalOr.phpwyYwvendor/nikic/php-parser/lib/PhpParser/Node/Expr/BitwiseNot.phpyY Ok>vendor/nikic/php-parser/lib/PhpParser/Node/Expr/BooleanNot.php&yY&Wbc?vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Cast/Array_.phphyYh>vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Cast/Bool_.phphyYh8g ?vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Cast/Double.phpiyYiۯڶ=vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Cast/Int_.phpgyYg6@vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Cast/Object_.phpjyYj>JW@vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Cast/String_.phpjyYjj*?vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Cast/Unset_.phphyYhb!8vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Cast.phpyY03Cvendor/nikic/php-parser/lib/PhpParser/Node/Expr/ClassConstFetch.phpyY,۶:vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Clone_.phpyYG;vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Closure.phpyY6 n>vendor/nikic/php-parser/lib/PhpParser/Node/Expr/ClosureUse.phpyY '>vendor/nikic/php-parser/lib/PhpParser/Node/Expr/ConstFetch.php8yY8::vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Empty_.phpyY ܾnAvendor/nikic/php-parser/lib/PhpParser/Node/Expr/ErrorSuppress.php yY c9vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Eval_.phpyY\9vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Exit_.phpyYH<vendor/nikic/php-parser/lib/PhpParser/Node/Expr/FuncCall.phpyY\DF<vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Include_.php+yY+W_T?vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Instanceof_.phpyY^:vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Isset_.phpyYf&q9vendor/nikic/php-parser/lib/PhpParser/Node/Expr/List_.phpJyYJQr>vendor/nikic/php-parser/lib/PhpParser/Node/Expr/MethodCall.phpyYCɀ8vendor/nikic/php-parser/lib/PhpParser/Node/Expr/New_.phpUyYUh ;vendor/nikic/php-parser/lib/PhpParser/Node/Expr/PostDec.phpyY X;vendor/nikic/php-parser/lib/PhpParser/Node/Expr/PostInc.phpyY9=:vendor/nikic/php-parser/lib/PhpParser/Node/Expr/PreDec.phpyYjIT:vendor/nikic/php-parser/lib/PhpParser/Node/Expr/PreInc.phpyYS,:vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Print_.phpyY0ͶAvendor/nikic/php-parser/lib/PhpParser/Node/Expr/PropertyFetch.phpyYmͶ=vendor/nikic/php-parser/lib/PhpParser/Node/Expr/ShellExec.php?yY?]V>vendor/nikic/php-parser/lib/PhpParser/Node/Expr/StaticCall.phpyYGvendor/nikic/php-parser/lib/PhpParser/Node/Expr/StaticPropertyFetch.phpyY»ɶ;vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Ternary.phppyYpCb>vendor/nikic/php-parser/lib/PhpParser/Node/Expr/UnaryMinus.phpyY=K=vendor/nikic/php-parser/lib/PhpParser/Node/Expr/UnaryPlus.php$yY$k<vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Variable.php%yY%a!:vendor/nikic/php-parser/lib/PhpParser/Node/Expr/Yield_.phpyY6=vendor/nikic/php-parser/lib/PhpParser/Node/Expr/YieldFrom.php(yY(5ڶ3vendor/nikic/php-parser/lib/PhpParser/Node/Expr.phpkyYkPp@;vendor/nikic/php-parser/lib/PhpParser/Node/FunctionLike.php>yY>wABvendor/nikic/php-parser/lib/PhpParser/Node/Name/FullyQualified.phpyYĝi<vendor/nikic/php-parser/lib/PhpParser/Node/Name/Relative.phpyYw3vendor/nikic/php-parser/lib/PhpParser/Node/Name.phpyYi3R4vendor/nikic/php-parser/lib/PhpParser/Node/Param.phpyY*=vendor/nikic/php-parser/lib/PhpParser/Node/Scalar/DNumber.phpyY(>vendor/nikic/php-parser/lib/PhpParser/Node/Scalar/Encapsed.php+yY+sHvendor/nikic/php-parser/lib/PhpParser/Node/Scalar/EncapsedStringPart.phpTyYT  =vendor/nikic/php-parser/lib/PhpParser/Node/Scalar/LNumber.php,yY, cGvendor/nikic/php-parser/lib/PhpParser/Node/Scalar/MagicConst/Class_.phpyY-(+<Dvendor/nikic/php-parser/lib/PhpParser/Node/Scalar/MagicConst/Dir.phpyY}uEvendor/nikic/php-parser/lib/PhpParser/Node/Scalar/MagicConst/File.phpyYJVJvendor/nikic/php-parser/lib/PhpParser/Node/Scalar/MagicConst/Function_.phpyYBEvendor/nikic/php-parser/lib/PhpParser/Node/Scalar/MagicConst/Line.phpyYMGvendor/nikic/php-parser/lib/PhpParser/Node/Scalar/MagicConst/Method.phpyYKvendor/nikic/php-parser/lib/PhpParser/Node/Scalar/MagicConst/Namespace_.phpyY"8FGvendor/nikic/php-parser/lib/PhpParser/Node/Scalar/MagicConst/Trait_.phpyY@vendor/nikic/php-parser/lib/PhpParser/Node/Scalar/MagicConst.php.yY.Ez=vendor/nikic/php-parser/lib/PhpParser/Node/Scalar/String_.phpyY&϶5vendor/nikic/php-parser/lib/PhpParser/Node/Scalar.phpHyYH/:vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Break_.phpNyYNŶ9vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Case_.phpyYs;}۶:vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Catch_.phpwyYw"3:vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Class_.php(yY(oԶ>vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/ClassConst.phpXyYXI=vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/ClassLike.phpuyYuWڳ?vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/ClassMethod.php_ yY_ Gж:vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Const_.phpNyYN$k=vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Continue_.phpZyYZ<<vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Declare_.phpyYBvendor/nikic/php-parser/lib/PhpParser/Node/Stmt/DeclareDeclare.phpyY/7vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Do_.phpyYRz9vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Echo_.php)yY)6+9vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Else_.php"yY"]}J;vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/ElseIf_.phpyY^(y8vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/For_.phpAyYAC<vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Foreach_.phpGyYG9Ƕ=vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Function_.phpxyYxu;vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Global_.php;yY;ն9vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Goto_.php(yY( +Å<vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/GroupUse.phpyYfY@vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/HaltCompiler.php}yY}7vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/If_.phpyY*>vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/InlineHTML.phpyY=\>vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Interface_.phpwyYwiI9vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Label.phpyY>vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Namespace_.phptyYt%7vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Nop.phpyY‘<vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Property.phpyYHDvendor/nikic/php-parser/lib/PhpParser/Node/Stmt/PropertyProperty.phpyY&y;vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Return_.php9yY9B ;vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Static_.phpHyYHcL=vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/StaticVar.phpyY1ា;vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Switch_.phpyYy:vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Throw_.php!yY!z:vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Trait_.php?yY?@<vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/TraitUse.phpyY5hWLvendor/nikic/php-parser/lib/PhpParser/Node/Stmt/TraitUseAdaptation/Alias.phpyY#Qvendor/nikic/php-parser/lib/PhpParser/Node/Stmt/TraitUseAdaptation/Precedence.phpeyYenQLFvendor/nikic/php-parser/lib/PhpParser/Node/Stmt/TraitUseAdaptation.phpyY݄嘶<vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/TryCatch.phpnyYnU:vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Unset_.php4yY4I8vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/Use_.phpyYVx:vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/UseUse.phpyYdU߶:vendor/nikic/php-parser/lib/PhpParser/Node/Stmt/While_.phpyYӥ3vendor/nikic/php-parser/lib/PhpParser/Node/Stmt.phpkyYkD.vendor/nikic/php-parser/lib/PhpParser/Node.phpyY %6vendor/nikic/php-parser/lib/PhpParser/NodeAbstract.phpyYx44vendor/nikic/php-parser/lib/PhpParser/NodeDumper.phpyYA#y7vendor/nikic/php-parser/lib/PhpParser/NodeTraverser.phpyY1@vendor/nikic/php-parser/lib/PhpParser/NodeTraverserInterface.phpyYjBvendor/nikic/php-parser/lib/PhpParser/NodeVisitor/NameResolver.php!yY! LW/5vendor/nikic/php-parser/lib/PhpParser/NodeVisitor.phpyY%(=vendor/nikic/php-parser/lib/PhpParser/NodeVisitorAbstract.php<yY<!S(̶9vendor/nikic/php-parser/lib/PhpParser/Parser/Multiple.phpyYF-P5vendor/nikic/php-parser/lib/PhpParser/Parser/Php5.phpkfyYkf3Z5vendor/nikic/php-parser/lib/PhpParser/Parser/Php7.phpyYN=7vendor/nikic/php-parser/lib/PhpParser/Parser/Tokens.phphyYhVKѶ0vendor/nikic/php-parser/lib/PhpParser/Parser.phptyYt-C8vendor/nikic/php-parser/lib/PhpParser/ParserAbstract.phpPyYPO{7vendor/nikic/php-parser/lib/PhpParser/ParserFactory.php-yY-*R@@vendor/nikic/php-parser/lib/PhpParser/PrettyPrinter/Standard.phpyYf 2?vendor/nikic/php-parser/lib/PhpParser/PrettyPrinterAbstract.php-yY-ݪc8vendor/nikic/php-parser/lib/PhpParser/Serializer/XML.phpC yYC yʲS4vendor/nikic/php-parser/lib/PhpParser/Serializer.phpyY{:vendor/nikic/php-parser/lib/PhpParser/Unserializer/XML.php8yY8}Ϳ6vendor/nikic/php-parser/lib/PhpParser/Unserializer.php yY v߶(vendor/nikic/php-parser/test_old/run.php*yY*3#vendor/pear/console_table/Table.phpXqyYXqZvendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Context.php5yY5l.%^vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Description.phpyYԲض[vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Location.phpqyYqu/]vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Serializer.phpyYrж`vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/AuthorTag.phpM yYM 1`vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/CoversTag.phpIyYI9{dvendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/DeprecatedTag.phpyYK avendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/ExampleTag.phpyY ^vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/LinkTag.phpLyYLF`vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/MethodTag.php~yY~!kͶ_vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/ParamTag.phpH yYH  fvendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/PropertyReadTag.php[yY[< =bvendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/PropertyTag.phpOyYO#mgvendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/PropertyWriteTag.php]yY]Rp`vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/ReturnTag.phpyYpS]vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/SeeTag.phpyYi?$_vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/SinceTag.php|yY|tR`vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/SourceTag.php yY n*`vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/ThrowsTag.phpLyYL"^vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/UsesTag.phpEyYE.˶]vendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/VarTag.phpEyYEͶavendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag/VersionTag.phpx yYx EƶVvendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Tag.php(yY(E6 bvendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock/Type/Collection.phpyY+=4Rvendor/phpdocumentor/reflection-docblock/src/phpDocumentor/Reflection/DocBlock.phpc6yYc6q/vendor/phpspec/prophecy/fixtures/EmptyClass.php:yY:=2X3vendor/phpspec/prophecy/fixtures/EmptyInterface.phpByYB/vendor/phpspec/prophecy/fixtures/FinalClass.php@yY@6vendor/phpspec/prophecy/fixtures/ModifierInterface.phpyY4*vendor/phpspec/prophecy/fixtures/Named.phpXyYXq$a6vendor/phpspec/prophecy/fixtures/OptionalDepsClass.phpyYoT3vendor/phpspec/prophecy/fixtures/SpecialMethods.phppyYp('2vendor/phpspec/prophecy/fixtures/WithArguments.phpyYEvendor/phpspec/prophecy/spec/Prophecy/Promise/CallbackPromiseSpec.php yY uKvendor/phpspec/prophecy/spec/Prophecy/Promise/ReturnArgumentPromiseSpec.phpyY޽Cvendor/phpspec/prophecy/spec/Prophecy/Promise/ReturnPromiseSpec.phpyY2tBvendor/phpspec/prophecy/spec/Prophecy/Promise/ThrowPromiseSpec.phpyY9Evendor/phpspec/prophecy/spec/Prophecy/Prophecy/MethodProphecySpec.php+1yY+1dgEvendor/phpspec/prophecy/spec/Prophecy/Prophecy/ObjectProphecySpec.phpA&yYA&1D1?vendor/phpspec/prophecy/spec/Prophecy/Prophecy/RevealerSpec.phpqyYqr5vendor/phpspec/prophecy/spec/Prophecy/ProphetSpec.phpV yYV N=vendor/phpspec/prophecy/spec/Prophecy/Util/StringUtilSpec.php yY 莶Cvendor/phpspec/prophecy/src/Prophecy/Argument/ArgumentsWildcard.php4 yY4 A;K2Fvendor/phpspec/prophecy/src/Prophecy/Argument/Token/AnyValuesToken.phpyYbN/Evendor/phpspec/prophecy/src/Prophecy/Argument/Token/AnyValueToken.phpyYFhMvendor/phpspec/prophecy/src/Prophecy/Argument/Token/ApproximateValueToken.phpyY#IGvendor/phpspec/prophecy/src/Prophecy/Argument/Token/ArrayCountToken.phpyY4̶Gvendor/phpspec/prophecy/src/Prophecy/Argument/Token/ArrayEntryToken.phpyYJ:Lvendor/phpspec/prophecy/src/Prophecy/Argument/Token/ArrayEveryEntryToken.phpyYpbEvendor/phpspec/prophecy/src/Prophecy/Argument/Token/CallbackToken.php,yY,cR̶Gvendor/phpspec/prophecy/src/Prophecy/Argument/Token/ExactValueToken.php yY 3Kvendor/phpspec/prophecy/src/Prophecy/Argument/Token/IdenticalValueToken.phpyYGvendor/phpspec/prophecy/src/Prophecy/Argument/Token/LogicalAndToken.phpyY NvGvendor/phpspec/prophecy/src/Prophecy/Argument/Token/LogicalNotToken.phpyYrHvendor/phpspec/prophecy/src/Prophecy/Argument/Token/ObjectStateToken.php9 yY9 E.Kvendor/phpspec/prophecy/src/Prophecy/Argument/Token/StringContainsToken.phpyY>Fvendor/phpspec/prophecy/src/Prophecy/Argument/Token/TokenInterface.phpyYٰAvendor/phpspec/prophecy/src/Prophecy/Argument/Token/TypeToken.phpyYn\1vendor/phpspec/prophecy/src/Prophecy/Argument.phpyYAT2vendor/phpspec/prophecy/src/Prophecy/Call/Call.php yY {:%8vendor/phpspec/prophecy/src/Prophecy/Call/CallCenter.phpyY|Evendor/phpspec/prophecy/src/Prophecy/Comparator/ClosureComparator.phpKyYK)RQ;vendor/phpspec/prophecy/src/Prophecy/Comparator/Factory.phpyYֈiFvendor/phpspec/prophecy/src/Prophecy/Comparator/ProphecyComparator.phpsyYshǶ>vendor/phpspec/prophecy/src/Prophecy/Doubler/CachedDoubler.phpyẎgOvendor/phpspec/prophecy/src/Prophecy/Doubler/ClassPatch/ClassPatchInterface.phplyYl)5:Svendor/phpspec/prophecy/src/Prophecy/Doubler/ClassPatch/DisableConstructorPatch.phpyY:0`Nvendor/phpspec/prophecy/src/Prophecy/Doubler/ClassPatch/HhvmExceptionPatch.phpyYx^Hvendor/phpspec/prophecy/src/Prophecy/Doubler/ClassPatch/KeywordPatch.php yY /@ȶJvendor/phpspec/prophecy/src/Prophecy/Doubler/ClassPatch/MagicCallPatch.phpm yYm 3.ŶPvendor/phpspec/prophecy/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php yY ni[vendor/phpspec/prophecy/src/Prophecy/Doubler/ClassPatch/ReflectionClassNewInstancePatch.phppyYpxLvendor/phpspec/prophecy/src/Prophecy/Doubler/ClassPatch/SplFileInfoPatch.phpQ yYQ [Lvendor/phpspec/prophecy/src/Prophecy/Doubler/ClassPatch/TraversablePatch.php yY jN@vendor/phpspec/prophecy/src/Prophecy/Doubler/DoubleInterface.phpyY8dj8vendor/phpspec/prophecy/src/Prophecy/Doubler/Doubler.phpyY8]^Mvendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php7 yY7 ОGvendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/ClassCreator.phpyY?BrFvendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/ClassMirror.phpyYՕcLvendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/Node/ArgumentNode.phpyY>XIvendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/Node/ClassNode.phpIyYI)UݶJvendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/Node/MethodNode.phpyY'׶Nvendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/ReflectionInterface.phpyY;vendor/phpspec/prophecy/src/Prophecy/Doubler/LazyDouble.phpF yYF l>vendor/phpspec/prophecy/src/Prophecy/Doubler/NameGenerator.phpyY7Ovendor/phpspec/prophecy/src/Prophecy/Exception/Call/UnexpectedCallException.phpyYPvendor/phpspec/prophecy/src/Prophecy/Exception/Doubler/ClassCreatorException.phpyY77/%Ovendor/phpspec/prophecy/src/Prophecy/Exception/Doubler/ClassMirrorException.phpyYۉ?Qvendor/phpspec/prophecy/src/Prophecy/Exception/Doubler/ClassNotFoundException.phpyYh+Jvendor/phpspec/prophecy/src/Prophecy/Exception/Doubler/DoubleException.phpyYzFKvendor/phpspec/prophecy/src/Prophecy/Exception/Doubler/DoublerException.phpyYZ^Uvendor/phpspec/prophecy/src/Prophecy/Exception/Doubler/InterfaceNotFoundException.phpyYWvendor/phpspec/prophecy/src/Prophecy/Exception/Doubler/MethodNotExtendableException.phpDyYDpRvendor/phpspec/prophecy/src/Prophecy/Exception/Doubler/MethodNotFoundException.phpyYihUvendor/phpspec/prophecy/src/Prophecy/Exception/Doubler/ReturnByReferenceException.phpyY<vendor/phpspec/prophecy/src/Prophecy/Exception/Exception.php+yY+Kvendor/phpspec/prophecy/src/Prophecy/Exception/InvalidArgumentException.phpyYgPvendor/phpspec/prophecy/src/Prophecy/Exception/Prediction/AggregateException.phpyY?D<ζWvendor/phpspec/prophecy/src/Prophecy/Exception/Prediction/FailedPredictionException.phpJyYJ~DNvendor/phpspec/prophecy/src/Prophecy/Exception/Prediction/NoCallsException.phpyYl<Qvendor/phpspec/prophecy/src/Prophecy/Exception/Prediction/PredictionException.phpyY2TѶ[vendor/phpspec/prophecy/src/Prophecy/Exception/Prediction/UnexpectedCallsCountException.phpyY ƶVvendor/phpspec/prophecy/src/Prophecy/Exception/Prediction/UnexpectedCallsException.php,yY,aSvendor/phpspec/prophecy/src/Prophecy/Exception/Prophecy/MethodProphecyException.php)yY)F4Svendor/phpspec/prophecy/src/Prophecy/Exception/Prophecy/ObjectProphecyException.phpyY:FMvendor/phpspec/prophecy/src/Prophecy/Exception/Prophecy/ProphecyException.phpyY$϶Tvendor/phpspec/prophecy/src/Prophecy/PhpDocumentor/ClassAndInterfaceTagRetriever.phpxyYxrЬHvendor/phpspec/prophecy/src/Prophecy/PhpDocumentor/ClassTagRetriever.phpDyYDd9϶Nvendor/phpspec/prophecy/src/Prophecy/PhpDocumentor/LegacyClassTagRetriever.phpoyYou9Rvendor/phpspec/prophecy/src/Prophecy/PhpDocumentor/MethodTagRetrieverInterface.phpyY ;Fvendor/phpspec/prophecy/src/Prophecy/Prediction/CallbackPrediction.phpyYVb{ζBvendor/phpspec/prophecy/src/Prophecy/Prediction/CallPrediction.phpQ yYQ IGvendor/phpspec/prophecy/src/Prophecy/Prediction/CallTimesPrediction.php yY XEvendor/phpspec/prophecy/src/Prophecy/Prediction/NoCallsPrediction.phpyYL9%Gvendor/phpspec/prophecy/src/Prophecy/Prediction/PredictionInterface.phpyY`IE@vendor/phpspec/prophecy/src/Prophecy/Promise/CallbackPromise.phpyY[Avendor/phpspec/prophecy/src/Prophecy/Promise/PromiseInterface.phpKyYKFvendor/phpspec/prophecy/src/Prophecy/Promise/ReturnArgumentPromise.php'yY'(>vendor/phpspec/prophecy/src/Prophecy/Promise/ReturnPromise.phpyY؏=vendor/phpspec/prophecy/src/Prophecy/Promise/ThrowPromise.php` yY` +,@vendor/phpspec/prophecy/src/Prophecy/Prophecy/MethodProphecy.php,yY,?@vendor/phpspec/prophecy/src/Prophecy/Prophecy/ObjectProphecy.phpyY5DCvendor/phpspec/prophecy/src/Prophecy/Prophecy/ProphecyInterface.php,yY,WJvendor/phpspec/prophecy/src/Prophecy/Prophecy/ProphecySubjectInterface.phpyYi:vendor/phpspec/prophecy/src/Prophecy/Prophecy/Revealer.phpyYjɸCvendor/phpspec/prophecy/src/Prophecy/Prophecy/RevealerInterface.phpHyYHgZ0vendor/phpspec/prophecy/src/Prophecy/Prophet.phpyYvq8vendor/phpspec/prophecy/src/Prophecy/Util/ExportUtil.phpPyYP2qƶ8vendor/phpspec/prophecy/src/Prophecy/Util/StringUtil.php yY %8vendor/phpunit/php-code-coverage/scripts/auto_append.phptyYts9vendor/phpunit/php-code-coverage/scripts/auto_prepend.phpyYAvendor/phpunit/php-code-coverage/src/CodeCoverage/Driver/HHVM.php^yY^7bCvendor/phpunit/php-code-coverage/src/CodeCoverage/Driver/PHPDBG.php yY 9Cvendor/phpunit/php-code-coverage/src/CodeCoverage/Driver/Xdebug.phpx yYx <vendor/phpunit/php-code-coverage/src/CodeCoverage/Driver.phpyY0Zvendor/phpunit/php-code-coverage/src/CodeCoverage/Exception/UnintentionallyCoveredCode.phpyY}?vendor/phpunit/php-code-coverage/src/CodeCoverage/Exception.phpyYK)<vendor/phpunit/php-code-coverage/src/CodeCoverage/Filter.php-yY-+XնCvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/Clover.php'yY'dVCvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/Crap4j.php,yY,G8EDvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/Factory.phpkyYkrp2Tvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php&yY&CʶTvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/Directory.php| yY| Ovendor/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/File.php,LyY,L=>Jvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer.php!yY!ԫuAvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML.php=yY=0Kvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/Node/Directory.phpS(yYS(Fvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/Node/File.phpJyYJQcJvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/Node/Iterator.phpyYMqAvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/Node.phpyY2޶@vendor/phpunit/php-code-coverage/src/CodeCoverage/Report/PHP.phpyY4R}Avendor/phpunit/php-code-coverage/src/CodeCoverage/Report/Text.phpx!yYx!`kJvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Directory.phpyYZNvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Coverage.php5yY5(+RLvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Method.phpuyYuʶLvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Report.phpCyYCY{ȶJvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Unit.phpB yYB  krEvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File.php0yY0ڶEvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Node.php^yY^NHvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Project.php]yY]h>Fvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Tests.phpyY2tGvendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Totals.phpyYoF@vendor/phpunit/php-code-coverage/src/CodeCoverage/Report/XML.phplyYlsPvendor/phpunit/php-code-coverage/src/CodeCoverage/Util/InvalidArgumentHelper.php=yY=:vendor/phpunit/php-code-coverage/src/CodeCoverage/Util.phpyYK϶5vendor/phpunit/php-code-coverage/src/CodeCoverage.phpsiyYsi]/vendor/phpunit/php-file-iterator/src/Facade.php yY 00vendor/phpunit/php-file-iterator/src/Factory.php yY y1vendor/phpunit/php-file-iterator/src/Iterator.phpayYa1vendor/phpunit/php-text-template/src/Template.php yY w4&vendor/phpunit/php-timer/src/Timer.phpE yYE 6Cvendor/phpunit/php-token-stream/src/Token/Stream/CachingFactory.phpyY_4vendor/phpunit/php-token-stream/src/Token/Stream.phpByYBxބ-vendor/phpunit/php-token-stream/src/Token.phpbyYbT2(vendor/phpunit/phpunit/src/Exception.phpsyYsy:۶8vendor/phpunit/phpunit/src/Extensions/GroupTestSuite.phpyYo`˶6vendor/phpunit/phpunit/src/Extensions/PhptTestCase.php3yY3>7vendor/phpunit/phpunit/src/Extensions/PhptTestSuite.phpyY ܶ6vendor/phpunit/phpunit/src/Extensions/RepeatedTest.phpyY7vendor/phpunit/phpunit/src/Extensions/TestDecorator.php@ yY@ 98vendor/phpunit/phpunit/src/Extensions/TicketListener.php%yY%"X9vendor/phpunit/phpunit/src/Framework/Assert/Functions.phpyY=[/vendor/phpunit/phpunit/src/Framework/Assert.php}yY}j2/=vendor/phpunit/phpunit/src/Framework/AssertionFailedError.php{yY{+9vendor/phpunit/phpunit/src/Framework/BaseTestListener.phppyYpK8X4>vendor/phpunit/phpunit/src/Framework/CodeCoverageException.phpqyYqE7vendor/phpunit/phpunit/src/Framework/Constraint/And.php6 yY6 Ŷ?vendor/phpunit/phpunit/src/Framework/Constraint/ArrayHasKey.phpyYV?vendor/phpunit/phpunit/src/Framework/Constraint/ArraySubset.phpIyYIc=vendor/phpunit/phpunit/src/Framework/Constraint/Attribute.php yY l<vendor/phpunit/phpunit/src/Framework/Constraint/Callback.php3yY3Evendor/phpunit/phpunit/src/Framework/Constraint/ClassHasAttribute.phpyYbKvendor/phpunit/phpunit/src/Framework/Constraint/ClassHasStaticAttribute.phpyY =vendor/phpunit/phpunit/src/Framework/Constraint/Composite.phpyY4~)9vendor/phpunit/phpunit/src/Framework/Constraint/Count.php yY u=vendor/phpunit/phpunit/src/Framework/Constraint/Exception.phpcyYc*/Avendor/phpunit/phpunit/src/Framework/Constraint/ExceptionCode.php[yY[ Dvendor/phpunit/phpunit/src/Framework/Constraint/ExceptionMessage.phpCyYCJvendor/phpunit/phpunit/src/Framework/Constraint/ExceptionMessageRegExp.php`yY`0}>vendor/phpunit/phpunit/src/Framework/Constraint/FileExists.phpyY+2?vendor/phpunit/phpunit/src/Framework/Constraint/GreaterThan.phpyYOR>vendor/phpunit/phpunit/src/Framework/Constraint/IsAnything.phppyYpV;vendor/phpunit/phpunit/src/Framework/Constraint/IsEmpty.php&yY&v;vendor/phpunit/phpunit/src/Framework/Constraint/IsEqual.php!yY! (Q;vendor/phpunit/phpunit/src/Framework/Constraint/IsFalse.phpuyYuֻ ?vendor/phpunit/phpunit/src/Framework/Constraint/IsIdentical.phpyYFy;@vendor/phpunit/phpunit/src/Framework/Constraint/IsInstanceOf.phpyY`Ѷ:vendor/phpunit/phpunit/src/Framework/Constraint/IsJson.phpyYz#l::vendor/phpunit/phpunit/src/Framework/Constraint/IsNull.phpqyYq^`:vendor/phpunit/phpunit/src/Framework/Constraint/IsTrue.phpqyYq*%:vendor/phpunit/phpunit/src/Framework/Constraint/IsType.phpt yYt KPTvendor/phpunit/phpunit/src/Framework/Constraint/JsonMatches/ErrorMessageProvider.phpyYd˜?vendor/phpunit/phpunit/src/Framework/Constraint/JsonMatches.php yY Ҷ<vendor/phpunit/phpunit/src/Framework/Constraint/LessThan.phpyY4w϶7vendor/phpunit/phpunit/src/Framework/Constraint/Not.phpyYRkFvendor/phpunit/phpunit/src/Framework/Constraint/ObjectHasAttribute.phpyY/6vendor/phpunit/phpunit/src/Framework/Constraint/Or.phpf yYf &=vendor/phpunit/phpunit/src/Framework/Constraint/PCREMatch.phpyY<vendor/phpunit/phpunit/src/Framework/Constraint/SameSize.phpSyYS/#SBvendor/phpunit/phpunit/src/Framework/Constraint/StringContains.php2yY2].Bvendor/phpunit/phpunit/src/Framework/Constraint/StringEndsWith.phpyYv>gAvendor/phpunit/phpunit/src/Framework/Constraint/StringMatches.php yY %cDvendor/phpunit/phpunit/src/Framework/Constraint/StringStartsWith.phpyY[Gvendor/phpunit/phpunit/src/Framework/Constraint/TraversableContains.php yY :Z5׶Kvendor/phpunit/phpunit/src/Framework/Constraint/TraversableContainsOnly.php yY g|s7vendor/phpunit/phpunit/src/Framework/Constraint/Xor.php yY G:3vendor/phpunit/phpunit/src/Framework/Constraint.php6yY6Q9vendor/phpunit/phpunit/src/Framework/Error/Deprecated.phpFyYFV5vendor/phpunit/phpunit/src/Framework/Error/Notice.php0yY0I6vendor/phpunit/phpunit/src/Framework/Error/Warning.php3yY3mO.vendor/phpunit/phpunit/src/Framework/Error.php$yY$X%yW2vendor/phpunit/phpunit/src/Framework/Exception.php yY ~b9vendor/phpunit/phpunit/src/Framework/ExceptionWrapper.phpyYCvendor/phpunit/phpunit/src/Framework/ExpectationFailedException.phpyY_S;7vendor/phpunit/phpunit/src/Framework/IncompleteTest.phpyYYx(;vendor/phpunit/phpunit/src/Framework/IncompleteTestCase.php5yY5.S<vendor/phpunit/phpunit/src/Framework/IncompleteTestError.phpyYHt Avendor/phpunit/phpunit/src/Framework/InvalidCoversTargetError.phpByYB |Evendor/phpunit/phpunit/src/Framework/InvalidCoversTargetException.phpyY%M4vendor/phpunit/phpunit/src/Framework/OutputError.phpyYj̶2vendor/phpunit/phpunit/src/Framework/RiskyTest.phpyY7vendor/phpunit/phpunit/src/Framework/RiskyTestError.phpyY?v7vendor/phpunit/phpunit/src/Framework/SelfDescribing.phpyYx/84vendor/phpunit/phpunit/src/Framework/SkippedTest.phpyYJQ8vendor/phpunit/phpunit/src/Framework/SkippedTestCase.phpyY`u9vendor/phpunit/phpunit/src/Framework/SkippedTestError.php yY T>vendor/phpunit/phpunit/src/Framework/SkippedTestSuiteError.phpyY7<7vendor/phpunit/phpunit/src/Framework/SyntheticError.phpyYƷ-vendor/phpunit/phpunit/src/Framework/Test.phpyY+lɶ1vendor/phpunit/phpunit/src/Framework/TestCase.phpyY44vendor/phpunit/phpunit/src/Framework/TestFailure.phpyY5vendor/phpunit/phpunit/src/Framework/TestListener.phpi yYi 3vendor/phpunit/phpunit/src/Framework/TestResult.phpmyYmjs?vendor/phpunit/phpunit/src/Framework/TestSuite/DataProvider.phpyYvendor/phpunit/phpunit/src/Util/TestDox/ResultPrinter/HTML.phpyYpfʶ>vendor/phpunit/phpunit/src/Util/TestDox/ResultPrinter/Text.phpwyYw ;9vendor/phpunit/phpunit/src/Util/TestDox/ResultPrinter.phpkyYk`B5vendor/phpunit/phpunit/src/Util/TestSuiteIterator.phpyY⍘(vendor/phpunit/phpunit/src/Util/Type.phpyYC!Ҷ'vendor/phpunit/phpunit/src/Util/XML.phpuyYuQvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Identity.phpyY(4Yvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/InvocationMocker.phpvyYvNvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Match.phpWyYWEAXvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/MethodNameMatch.php)yY)sRvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Namespace.phpyYM쉔Xvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/ParametersMatch.phpyYsґζMvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Stub.phpoyYohravendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/BadMethodCallException.phpyY).Tvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/Exception.phpyY]T[vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/RuntimeException.phpyYYn4Jvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator.phpyY Rvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Object.phpyY9Rvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Static.phpFyYFKpKvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation.phpyYsQvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/InvocationMocker.phpyY2׶Jvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invokable.php9yY9~Xvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyInvokedCount.phpyYDs$Vvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyParameters.phpCyYC^vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/ConsecutiveParameters.phpyYiOmSvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Invocation.phpyYKbWvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtIndex.php yY Y\vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastCount.phpyY R[vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastOnce.phpyYcI[vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtMostCount.phpyYUvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedCount.php yY SGXvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedRecorder.php`yY`!Svendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/MethodName.php yY vSvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Parameters.phpyYPj\vendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/StatelessInvocation.phpkyYk϶Hvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher.php yY 7/Lvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockBuilder.phpyY\_Kvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockObject.php9yY9CVvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ConsecutiveCalls.phpyY2 Ovendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Exception.phpyY<Wvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/MatcherCollection.php:yY:YLvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Return.phpyYLf+Tvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnArgument.phpyY9)`Tvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnCallback.phpyYg`Pvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnSelf.phpyYITvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnValueMap.php|yY|uB Evendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub.php\yY\f+FжKvendor/phpunit/phpunit-mock-objects/src/Framework/MockObject/Verifiable.phpyY'L)vendor/psr/log/Psr/Log/AbstractLogger.php yY Gl3vendor/psr/log/Psr/Log/InvalidArgumentException.php`yY` X1/vendor/psr/log/Psr/Log/LoggerAwareInterface.php)yY)j +vendor/psr/log/Psr/Log/LoggerAwareTrait.phpyYz%*vendor/psr/log/Psr/Log/LoggerInterface.php yY ?}&vendor/psr/log/Psr/Log/LoggerTrait.php yY ý#vendor/psr/log/Psr/Log/LogLevel.phpPyYP%vendor/psr/log/Psr/Log/NullLogger.phpyYZf'vendor/psy/psysh/src/Psy/Autoloader.phpQyYQV:vendor/psy/psysh/src/Psy/CodeCleaner/AbstractClassPass.phpyY]?vendor/psy/psysh/src/Psy/CodeCleaner/AssignThisVariablePass.phpyYka(8vendor/psy/psysh/src/Psy/CodeCleaner/CalledClassPass.phpv yYv ڂ)/Dvendor/psy/psysh/src/Psy/CodeCleaner/CallTimePassByReferencePass.phpyYQhK8vendor/psy/psysh/src/Psy/CodeCleaner/CodeCleanerPass.phpyY_1vendor/psy/psysh/src/Psy/CodeCleaner/ExitPass.phpLyYL3=M7Ivendor/psy/psysh/src/Psy/CodeCleaner/FunctionReturnInWriteContextPass.phpC yYC Ko;];vendor/psy/psysh/src/Psy/CodeCleaner/ImplicitReturnPass.phpyYlljG7vendor/psy/psysh/src/Psy/CodeCleaner/InstanceOfPass.phpyY5v<vendor/psy/psysh/src/Psy/CodeCleaner/LeavePsyshAlonePass.phpyYHa 8vendor/psy/psysh/src/Psy/CodeCleaner/LegacyEmptyPass.php2yY2qn;vendor/psy/psysh/src/Psy/CodeCleaner/MagicConstantsPass.phpEyYE3;vendor/psy/psysh/src/Psy/CodeCleaner/NamespaceAwarePass.phpfyYf.6vendor/psy/psysh/src/Psy/CodeCleaner/NamespacePass.phpyY@vendor/psy/psysh/src/Psy/CodeCleaner/PassableByReferencePass.phpx yYx O@Mq>vendor/psy/psysh/src/Psy/CodeCleaner/StaticConstructorPass.php yY M#l8vendor/psy/psysh/src/Psy/CodeCleaner/StrictTypesPass.php yY -=9vendor/psy/psysh/src/Psy/CodeCleaner/UseStatementPass.php yY T]r=;vendor/psy/psysh/src/Psy/CodeCleaner/ValidClassNamePass.php*yY*`!JŶ:vendor/psy/psysh/src/Psy/CodeCleaner/ValidConstantPass.php yY l>vendor/psy/psysh/src/Psy/CodeCleaner/ValidFunctionNamePass.phpC yYC R(vendor/psy/psysh/src/Psy/CodeCleaner.phpyYLO2vendor/psy/psysh/src/Psy/Command/BufferCommand.phpyYK1vendor/psy/psysh/src/Psy/Command/ClearCommand.phpyY0 ,vendor/psy/psysh/src/Psy/Command/Command.phpyY/vendor/psy/psysh/src/Psy/Command/DocCommand.php yY s0vendor/psy/psysh/src/Psy/Command/DumpCommand.php- yY- \c0vendor/psy/psysh/src/Psy/Command/ExitCommand.phpGyYGi&0vendor/psy/psysh/src/Psy/Command/HelpCommand.php yY 63vendor/psy/psysh/src/Psy/Command/HistoryCommand.php"yY")Hvendor/psy/psysh/src/Psy/Command/ListCommand/ClassConstantEnumerator.php yY A@vendor/psy/psysh/src/Psy/Command/ListCommand/ClassEnumerator.phpyY}+Cvendor/psy/psysh/src/Psy/Command/ListCommand/ConstantEnumerator.php yY g;vendor/psy/psysh/src/Psy/Command/ListCommand/Enumerator.phpeyYe ϶Cvendor/psy/psysh/src/Psy/Command/ListCommand/FunctionEnumerator.php yY *Ivendor/psy/psysh/src/Psy/Command/ListCommand/GlobalVariableEnumerator.phpyY):hRDvendor/psy/psysh/src/Psy/Command/ListCommand/InterfaceEnumerator.php[yY[}Avendor/psy/psysh/src/Psy/Command/ListCommand/MethodEnumerator.phpv yYv Q$Cvendor/psy/psysh/src/Psy/Command/ListCommand/PropertyEnumerator.phpyY@vendor/psy/psysh/src/Psy/Command/ListCommand/TraitEnumerator.phpyYq Cvendor/psy/psysh/src/Psy/Command/ListCommand/VariableEnumerator.php yY g0vendor/psy/psysh/src/Psy/Command/ListCommand.php(yY( W۶1vendor/psy/psysh/src/Psy/Command/ParseCommand.phpyYŤ6vendor/psy/psysh/src/Psy/Command/PsyVersionCommand.phpyY0P6vendor/psy/psysh/src/Psy/Command/ReflectingCommand.phpyY6A&0vendor/psy/psysh/src/Psy/Command/ShowCommand.phpyYN3vendor/psy/psysh/src/Psy/Command/ThrowUpCommand.phpyYl1vendor/psy/psysh/src/Psy/Command/TraceCommand.phpyYж4vendor/psy/psysh/src/Psy/Command/WhereamiCommand.phpO yYO *:/vendor/psy/psysh/src/Psy/Command/WtfCommand.php yY Ƶ%vendor/psy/psysh/src/Psy/Compiler.phpyYp/*(vendor/psy/psysh/src/Psy/ConfigPaths.phpyYҫ]]*vendor/psy/psysh/src/Psy/Configuration.phpzyYzOT0vendor/psy/psysh/src/Psy/ConsoleColorFactory.phpiyYi?)$vendor/psy/psysh/src/Psy/Context.php yY vj^)vendor/psy/psysh/src/Psy/ContextAware.php7yY7[5vendor/psy/psysh/src/Psy/Exception/BreakException.phpGyYGf,n:vendor/psy/psysh/src/Psy/Exception/DeprecatedException.php~yY~I1϶5vendor/psy/psysh/src/Psy/Exception/ErrorException.php yY p330vendor/psy/psysh/src/Psy/Exception/Exception.phpyYDE :vendor/psy/psysh/src/Psy/Exception/FatalErrorException.php%yY%-y՛:vendor/psy/psysh/src/Psy/Exception/ParseErrorException.phpyYt7vendor/psy/psysh/src/Psy/Exception/RuntimeException.phpyYo(Ķ7vendor/psy/psysh/src/Psy/Exception/ThrowUpException.php{yY{W9vendor/psy/psysh/src/Psy/Exception/TypeErrorException.phpyY@6vendor/psy/psysh/src/Psy/ExecutionLoop/ForkingLoop.phpTyYT}/vendor/psy/psysh/src/Psy/ExecutionLoop/Loop.phpyYacڹ4vendor/psy/psysh/src/Psy/Formatter/CodeFormatter.phpyY{X8vendor/psy/psysh/src/Psy/Formatter/DocblockFormatter.phpyY<0vendor/psy/psysh/src/Psy/Formatter/Formatter.phpyY.,9vendor/psy/psysh/src/Psy/Formatter/SignatureFormatter.phpyY" &vendor/psy/psysh/src/Psy/functions.phpyYg@p/vendor/psy/psysh/src/Psy/Output/OutputPager.php6yY6,z$1vendor/psy/psysh/src/Psy/Output/PassthruPager.php)yY)p@3vendor/psy/psysh/src/Psy/Output/ProcOutputPager.php yY B/vendor/psy/psysh/src/Psy/Output/ShellOutput.php yY Yfa*vendor/psy/psysh/src/Psy/ParserFactory.php yY *=1vendor/psy/psysh/src/Psy/Readline/GNUReadline.phpKyYK,ȡ90vendor/psy/psysh/src/Psy/Readline/HoaConsole.php!yY!rR-vendor/psy/psysh/src/Psy/Readline/Libedit.php yY P.vendor/psy/psysh/src/Psy/Readline/Readline.phpyYW1/vendor/psy/psysh/src/Psy/Readline/Transient.php yY fe:vendor/psy/psysh/src/Psy/Reflection/ReflectionConstant.php yY 7Cvendor/psy/psysh/src/Psy/Reflection/ReflectionLanguageConstruct.php yY Lvendor/psy/psysh/src/Psy/Reflection/ReflectionLanguageConstructParameter.phpgyYgcT"vendor/psy/psysh/src/Psy/Shell.phpeyYeu8vendor/psy/psysh/src/Psy/TabCompletion/AutoCompleter.php yY ݬNvendor/psy/psysh/src/Psy/TabCompletion/Matcher/AbstractContextAwareMatcher.php%yY%9/7Bvendor/psy/psysh/src/Psy/TabCompletion/Matcher/AbstractMatcher.php~yY~20Ivendor/psy/psysh/src/Psy/TabCompletion/Matcher/ClassAttributesMatcher.phpyY*Fvendor/psy/psysh/src/Psy/TabCompletion/Matcher/ClassMethodsMatcher.phpeyYe޶Dvendor/psy/psysh/src/Psy/TabCompletion/Matcher/ClassNamesMatcher.php^ yY^ fo{Bvendor/psy/psysh/src/Psy/TabCompletion/Matcher/CommandsMatcher.php yY `Cvendor/psy/psysh/src/Psy/TabCompletion/Matcher/ConstantsMatcher.phpEyYEüi}Cvendor/psy/psysh/src/Psy/TabCompletion/Matcher/FunctionsMatcher.php|yY|ƠIBvendor/psy/psysh/src/Psy/TabCompletion/Matcher/KeywordsMatcher.php<yY<aEvendor/psy/psysh/src/Psy/TabCompletion/Matcher/MongoClientMatcher.phpyYlGvendor/psy/psysh/src/Psy/TabCompletion/Matcher/MongoDatabaseMatcher.phpyYԶJvendor/psy/psysh/src/Psy/TabCompletion/Matcher/ObjectAttributesMatcher.phpCyYC蠶Gvendor/psy/psysh/src/Psy/TabCompletion/Matcher/ObjectMethodsMatcher.php&yY&<Cvendor/psy/psysh/src/Psy/TabCompletion/Matcher/VariablesMatcher.phpyYڮ.7*vendor/psy/psysh/src/Psy/Util/Docblock.phpyY c&vendor/psy/psysh/src/Psy/Util/Json.phpyYo(vendor/psy/psysh/src/Psy/Util/Mirror.php yY %vendor/psy/psysh/src/Psy/Util/Str.php yY 6c-vendor/psy/psysh/src/Psy/VarDumper/Cloner.phpryYrö-vendor/psy/psysh/src/Psy/VarDumper/Dumper.php yY #0vendor/psy/psysh/src/Psy/VarDumper/Presenter.php yY D5vendor/psy/psysh/src/Psy/VarDumper/PresenterAware.phpyY 3vendor/psy/psysh/src/Psy/VersionUpdater/Checker.php(yY(S&ʶ9vendor/psy/psysh/src/Psy/VersionUpdater/GitHubChecker.phpRyYRy믶;vendor/psy/psysh/src/Psy/VersionUpdater/IntervalChecker.phpyYsl7vendor/psy/psysh/src/Psy/VersionUpdater/NoopChecker.php]yY]}u83vendor/sebastian/comparator/src/ArrayComparator.phpyYvx.vendor/sebastian/comparator/src/Comparator.phpPyYP!P=5vendor/sebastian/comparator/src/ComparisonFailure.php yY V6vendor/sebastian/comparator/src/DateTimeComparator.php yY 5vendor/sebastian/comparator/src/DOMNodeComparator.php yY >%4vendor/sebastian/comparator/src/DoubleComparator.php7yY7Stٶ7vendor/sebastian/comparator/src/ExceptionComparator.phpyYkf+vendor/sebastian/comparator/src/Factory.phpd yYd ه18vendor/sebastian/comparator/src/MockObjectComparator.phpyYO5vendor/sebastian/comparator/src/NumericComparator.php~ yY~ 74vendor/sebastian/comparator/src/ObjectComparator.phpyYw6vendor/sebastian/comparator/src/ResourceComparator.phpyY&w4vendor/sebastian/comparator/src/ScalarComparator.php>yY>B(>vendor/sebastian/comparator/src/SplObjectStorageComparator.php yY MQ2vendor/sebastian/comparator/src/TypeComparator.phpyYQ#vendor/sebastian/diff/src/Chunk.phpyY-*"vendor/sebastian/diff/src/Diff.phpyY˾$vendor/sebastian/diff/src/Differ.phpyY`:vendor/sebastian/diff/src/LCS/LongestCommonSubsequence.phplyYll1Wvendor/sebastian/diff/src/LCS/MemoryEfficientLongestCommonSubsequenceImplementation.php yY _Uvendor/sebastian/diff/src/LCS/TimeEfficientLongestCommonSubsequenceImplementation.phpyYf#5/"vendor/sebastian/diff/src/Line.phpyY:;$vendor/sebastian/diff/src/Parser.php' yY' ZK,vendor/sebastian/environment/src/Console.php yY ?.",vendor/sebastian/environment/src/Runtime.phpyYnѦ*vendor/sebastian/exporter/src/Exporter.php#yY## H/vendor/sebastian/global-state/src/Blacklist.php[ yY[ :2vendor/sebastian/global-state/src/CodeExporter.phpyY`(C/vendor/sebastian/global-state/src/Exception.php?yY?.vendor/sebastian/global-state/src/Restorer.phpyYӓ;\6vendor/sebastian/global-state/src/RuntimeException.phpqyYq~]!.vendor/sebastian/global-state/src/Snapshot.php%yY%ÖSݶ2vendor/sebastian/recursion-context/src/Context.phpyYqP4vendor/sebastian/recursion-context/src/Exception.phpJyYJCvendor/sebastian/recursion-context/src/InvalidArgumentException.phpyYmH(vendor/sebastian/version/src/Version.php2yY2BZ&vendor/symfony/console/Application.phpyYD$*vendor/symfony/console/Command/Command.phpMyYMj*.vendor/symfony/console/Command/HelpCommand.php yY '.vendor/symfony/console/Command/ListCommand.phpo yYo qX(vendor/symfony/console/ConsoleEvents.php7yY7E<vendor/symfony/console/Descriptor/ApplicationDescription.phpyY0vendor/symfony/console/Descriptor/Descriptor.php yY ɠ9vendor/symfony/console/Descriptor/DescriptorInterface.phpyYJZ0<4vendor/symfony/console/Descriptor/JsonDescriptor.phpyYt[˶8vendor/symfony/console/Descriptor/MarkdownDescriptor.phpyYDy˶4vendor/symfony/console/Descriptor/TextDescriptor.php-(yY-(pʔ3vendor/symfony/console/Descriptor/XmlDescriptor.phpS%yYS%N4vendor/symfony/console/Event/ConsoleCommandEvent.php=yY=%mf-vendor/symfony/console/Event/ConsoleEvent.phpyY 6vendor/symfony/console/Event/ConsoleExceptionEvent.php=yY=Ŷ6vendor/symfony/console/Event/ConsoleTerminateEvent.phpyY{e=vendor/symfony/console/Exception/CommandNotFoundException.phpyY7vendor/symfony/console/Exception/ExceptionInterface.phpyYU=vendor/symfony/console/Exception/InvalidArgumentException.phpyYu i;vendor/symfony/console/Exception/InvalidOptionException.phpyY;3vendor/symfony/console/Exception/LogicException.phpyYSML5vendor/symfony/console/Exception/RuntimeException.phpyY*b4vendor/symfony/console/Formatter/OutputFormatter.php?yY?=vendor/symfony/console/Formatter/OutputFormatterInterface.php]yY]y L9vendor/symfony/console/Formatter/OutputFormatterStyle.phpyYD1Bvendor/symfony/console/Formatter/OutputFormatterStyleInterface.php[yY[f@>vendor/symfony/console/Formatter/OutputFormatterStyleStack.php& yY& ]46vendor/symfony/console/Helper/DebugFormatterHelper.phpVyYVݔ2vendor/symfony/console/Helper/DescriptorHelper.php` yY` +7*Ŷ.vendor/symfony/console/Helper/DialogHelper.phpDyYD1vendor/symfony/console/Helper/FormatterHelper.php yY "*(vendor/symfony/console/Helper/Helper.php yY 1vendor/symfony/console/Helper/HelperInterface.phpyY+vendor/symfony/console/Helper/HelperSet.phpyYo2vendor/symfony/console/Helper/InputAwareHelper.phpyY˶/vendor/symfony/console/Helper/ProcessHelper.phpyYt-vendor/symfony/console/Helper/ProgressBar.php"FyY"F?@}Ͷ0vendor/symfony/console/Helper/ProgressHelper.phpO1yYO1kLĶ3vendor/symfony/console/Helper/ProgressIndicator.php!yY!"0vendor/symfony/console/Helper/QuestionHelper.phpe5yYe5j7vendor/symfony/console/Helper/SymfonyQuestionHelper.phpyYQ "'vendor/symfony/console/Helper/Table.phpHyYH{.l+vendor/symfony/console/Helper/TableCell.phpVyYV%-vendor/symfony/console/Helper/TableHelper.phpoyYo3ܶ0vendor/symfony/console/Helper/TableSeparator.phpEyYEp|ն,vendor/symfony/console/Helper/TableStyle.phpyYk*vendor/symfony/console/Input/ArgvInput.php'yY'/b+vendor/symfony/console/Input/ArrayInput.phpyYCV&vendor/symfony/console/Input/Input.phpyY0.vendor/symfony/console/Input/InputArgument.php yY mB4vendor/symfony/console/Input/InputAwareInterface.php^yY^9Kh0vendor/symfony/console/Input/InputDefinition.phpA3yYA3;/vendor/symfony/console/Input/InputInterface.phpNyYN(Γ,vendor/symfony/console/Input/InputOption.phpwyYw8s,vendor/symfony/console/Input/StringInput.php yY {퉭/vendor/symfony/console/Logger/ConsoleLogger.phpyYDY0vendor/symfony/console/Output/BufferedOutput.phphyYht|X4/vendor/symfony/console/Output/ConsoleOutput.phpyYw8vendor/symfony/console/Output/ConsoleOutputInterface.phpKyYK0,vendor/symfony/console/Output/NullOutput.phpnyYntD(vendor/symfony/console/Output/Output.phpyY„1vendor/symfony/console/Output/OutputInterface.phpu yYu ʶ.vendor/symfony/console/Output/StreamOutput.php yY W2vendor/symfony/console/Question/ChoiceQuestion.phpyYnx8vendor/symfony/console/Question/ConfirmationQuestion.php7yY7&,vendor/symfony/console/Question/Question.phpyY`*׶ vendor/symfony/console/Shell.phpyY+,,vendor/symfony/console/Style/OutputStyle.php0 yY0 B޿/vendor/symfony/console/Style/StyleInterface.php yY 3Z-vendor/symfony/console/Style/SymfonyStyle.php>/yY>/ (vendor/symfony/debug/BufferingLogger.phpyYM0 =vendor/symfony/debug/Debug.phpyY-)vendor/symfony/debug/DebugClassLoader.php3yY3઄%vendor/symfony/debug/ErrorHandler.phpyyYy.669vendor/symfony/debug/Exception/ClassNotFoundException.php<yY<'D8vendor/symfony/debug/Exception/ContextErrorException.phpkyYkG 1vendor/symfony/debug/Exception/DummyException.phpOyYO"n 6vendor/symfony/debug/Exception/FatalErrorException.php yY [ }6vendor/symfony/debug/Exception/FatalThrowableError.php4yY43vendor/symfony/debug/Exception/FlattenException.phpW yYW B7vendor/symfony/debug/Exception/OutOfMemoryException.phpyY h=vendor/symfony/debug/Exception/UndefinedFunctionException.php+yY+O;vendor/symfony/debug/Exception/UndefinedMethodException.php&yY&ïy߶)vendor/symfony/debug/ExceptionHandler.php9KyY9KV4Ivendor/symfony/debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.phpkyYk舮&Evendor/symfony/debug/FatalErrorHandler/FatalErrorHandlerInterface.phpyYiAMvendor/symfony/debug/FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php yY gKvendor/symfony/debug/FatalErrorHandler/UndefinedMethodFatalErrorHandler.php>yY>˶Avendor/symfony/event-dispatcher/ContainerAwareEventDispatcher.phpyYpxBvendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php,yY,YtvKvendor/symfony/event-dispatcher/Debug/TraceableEventDispatcherInterface.php#yY#Ӗc9vendor/symfony/event-dispatcher/Debug/WrappedListener.phpyYM} Mvendor/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.phpXyYX6m)vendor/symfony/event-dispatcher/Event.php yY L3vendor/symfony/event-dispatcher/EventDispatcher.phpyYЁ<vendor/symfony/event-dispatcher/EventDispatcherInterface.php yY <vendor/symfony/event-dispatcher/EventSubscriberInterface.phpyY0vendor/symfony/event-dispatcher/GenericEvent.php%yY%Zn<vendor/symfony/event-dispatcher/ImmutableEventDispatcher.php~ yY~ (1vendor/symfony/finder/Adapter/AbstractAdapter.phpyY Ѷ5vendor/symfony/finder/Adapter/AbstractFindAdapter.php*yY**+2vendor/symfony/finder/Adapter/AdapterInterface.php yY mTA0vendor/symfony/finder/Adapter/BsdFindAdapter.php yY OH0vendor/symfony/finder/Adapter/GnuFindAdapter.php yY f!,vendor/symfony/finder/Adapter/PhpAdapter.phpq yYq /vendor/symfony/finder/Comparator/Comparator.phpyY3vendor/symfony/finder/Comparator/DateComparator.phpyY 'e5vendor/symfony/finder/Comparator/NumberComparator.php. yY. BpC9vendor/symfony/finder/Exception/AccessDeniedException.phpyYcW޶;vendor/symfony/finder/Exception/AdapterFailureException.phpKyYK|^6vendor/symfony/finder/Exception/ExceptionInterface.phpyY7Avendor/symfony/finder/Exception/OperationNotPermitedException.phpryYrc @vendor/symfony/finder/Exception/ShellCommandFailureException.phpyY~˶/vendor/symfony/finder/Expression/Expression.php yY \Aݶ)vendor/symfony/finder/Expression/Glob.phpDyYD^b*vendor/symfony/finder/Expression/Regex.phpyYFH3vendor/symfony/finder/Expression/ValueInterface.phpyYvV׶ vendor/symfony/finder/Finder.phpdyYdvendor/symfony/finder/Glob.php yY R`7vendor/symfony/finder/Iterator/CustomFilterIterator.phpyYg3,:vendor/symfony/finder/Iterator/DateRangeFilterIterator.phpyYF;vendor/symfony/finder/Iterator/DepthRangeFilterIterator.phpyY-,Avendor/symfony/finder/Iterator/ExcludeDirectoryFilterIterator.php yY D˶<vendor/symfony/finder/Iterator/FilecontentFilterIterator.phpyYr~9vendor/symfony/finder/Iterator/FilenameFilterIterator.phpyY p4vendor/symfony/finder/Iterator/FilePathsIterator.phpP yYP 9vendor/symfony/finder/Iterator/FileTypeFilterIterator.phpYyYY]%1vendor/symfony/finder/Iterator/FilterIterator.phpyYA;=vendor/symfony/finder/Iterator/MultiplePcreFilterIterator.php yY qж5vendor/symfony/finder/Iterator/PathFilterIterator.phpyY=vendor/symfony/finder/Iterator/RecursiveDirectoryIterator.phpyY:vendor/symfony/finder/Iterator/SizeRangeFilterIterator.phpyY3vendor/symfony/finder/Iterator/SortableIterator.php7 yY7 ׌0'vendor/symfony/finder/Shell/Command.phpsyYs Q+%vendor/symfony/finder/Shell/Shell.phpKyYK5%vendor/symfony/finder/SplFileInfo.phpuyYu"P.vendor/symfony/polyfill-mbstring/bootstrap.php'yY'p#Di-vendor/symfony/polyfill-mbstring/Mbstring.phpSyYS ]@vendor/symfony/polyfill-mbstring/Resources/unidata/lowerCase.phpIyYI҈@vendor/symfony/polyfill-mbstring/Resources/unidata/upperCase.php9JyY9J|Q7vendor/symfony/process/Exception/ExceptionInterface.phpyY<=vendor/symfony/process/Exception/InvalidArgumentException.phpyY˅3vendor/symfony/process/Exception/LogicException.phpyYW;vendor/symfony/process/Exception/ProcessFailedException.phpEyYEkҐ=vendor/symfony/process/Exception/ProcessTimedOutException.php{yY{25vendor/symfony/process/Exception/RuntimeException.phpyY>H+vendor/symfony/process/ExecutableFinder.php yY 1?׶.vendor/symfony/process/PhpExecutableFinder.phpyYl(%vendor/symfony/process/PhpProcess.php} yY} Ը.vendor/symfony/process/Pipes/AbstractPipes.phpeyYe;̶/vendor/symfony/process/Pipes/PipesInterface.phptyYt?_*vendor/symfony/process/Pipes/UnixPipes.phpyY-vendor/symfony/process/Pipes/WindowsPipes.phpyY`"vendor/symfony/process/Process.phpyYSD)vendor/symfony/process/ProcessBuilder.phpyY~'vendor/symfony/process/ProcessUtils.phpyYf/vendor/symfony/var-dumper/Caster/AmqpCaster.phpfyYfn+vendor/symfony/var-dumper/Caster/Caster.phpyY+mX.vendor/symfony/var-dumper/Caster/ConstStub.phpEyYER1vendor/symfony/var-dumper/Caster/CutArrayStub.phpyY*Ƭ,vendor/symfony/var-dumper/Caster/CutStub.phpyY|R3vendor/symfony/var-dumper/Caster/DoctrineCaster.phpxyYxN.vendor/symfony/var-dumper/Caster/DOMCaster.php&yY&W$>-vendor/symfony/var-dumper/Caster/EnumStub.php#yY#g(4vendor/symfony/var-dumper/Caster/ExceptionCaster.php)yY) Ķ.vendor/symfony/var-dumper/Caster/FrameStub.phpyYS,0vendor/symfony/var-dumper/Caster/MongoCaster.php;yY;ouԶ.vendor/symfony/var-dumper/Caster/PdoCaster.php yY mj@0vendor/symfony/var-dumper/Caster/PgSqlCaster.phpRyYR?05vendor/symfony/var-dumper/Caster/ReflectionCaster.php_*yY_*W𫕶3vendor/symfony/var-dumper/Caster/ResourceCaster.phpyYo>K.vendor/symfony/var-dumper/Caster/SplCaster.phpyY0Ȑ/vendor/symfony/var-dumper/Caster/StubCaster.phpyYc.vendor/symfony/var-dumper/Caster/TraceStub.phpyY~6vendor/symfony/var-dumper/Caster/XmlResourceCaster.php yY #w3vendor/symfony/var-dumper/Cloner/AbstractCloner.php8yY88c4vendor/symfony/var-dumper/Cloner/ClonerInterface.php<yY<0F+vendor/symfony/var-dumper/Cloner/Cursor.phpyYACx)vendor/symfony/var-dumper/Cloner/Data.php yY Oa4vendor/symfony/var-dumper/Cloner/DumperInterface.phpyY^Y)vendor/symfony/var-dumper/Cloner/Stub.phpuyYu .vendor/symfony/var-dumper/Cloner/VarCloner.php8yY8 3vendor/symfony/var-dumper/Dumper/AbstractDumper.phpyYr.vendor/symfony/var-dumper/Dumper/CliDumper.php=yY=68vendor/symfony/var-dumper/Dumper/DataDumperInterface.phpSyYSv%/vendor/symfony/var-dumper/Dumper/HtmlDumper.php5yY5B7K?vendor/symfony/var-dumper/Exception/ThrowingCasterException.phpyY#6vendor/symfony/var-dumper/Resources/functions/dump.phpyYK]'vendor/symfony/var-dumper/VarDumper.phpGyYGvendor/symfony/yaml/Dumper.php yY vendor/symfony/yaml/Escaper.phpyY8Ǧ/vendor/symfony/yaml/Exception/DumpException.phpyY4vendor/symfony/yaml/Exception/ExceptionInterface.phpyY^KA0vendor/symfony/yaml/Exception/ParseException.php yY !2vendor/symfony/yaml/Exception/RuntimeException.phpyY_qvendor/symfony/yaml/Inline.php$SyY$S@xvendor/symfony/yaml/Parser.phpX}yYX},!vendor/symfony/yaml/Unescaper.phpDyYDxvendor/symfony/yaml/Yaml.phpyY[?vendor/victorjonsson/markdowndocs/src/PHPDocsMD/ClassEntity.phpyY8tdFvendor/victorjonsson/markdowndocs/src/PHPDocsMD/ClassEntityFactory.php,yY,|>vendor/victorjonsson/markdowndocs/src/PHPDocsMD/CodeEntity.phpyYA?vendor/victorjonsson/markdowndocs/src/PHPDocsMD/Console/CLI.phpyY Lvendor/victorjonsson/markdowndocs/src/PHPDocsMD/Console/PHPDocsMDCommand.php%yY%G~;vendor/victorjonsson/markdowndocs/src/PHPDocsMD/DocInfo.php yY CDvendor/victorjonsson/markdowndocs/src/PHPDocsMD/DocInfoExtractor.php}yY}lBvendor/victorjonsson/markdowndocs/src/PHPDocsMD/FunctionEntity.phpyY#UֶBvendor/victorjonsson/markdowndocs/src/PHPDocsMD/FunctionFinder.phpfyYfDvendor/victorjonsson/markdowndocs/src/PHPDocsMD/MDTableGenerator.php yY ,q?vendor/victorjonsson/markdowndocs/src/PHPDocsMD/ParamEntity.phpyY=hy'commands/core/drupal/site_install_7.inc yY commands/core/drupal/update.incj7yYj7$!commands/core/drupal/update_6.inclLyYlLab!commands/core/drupal/update_7.inc-yY-;commands/core/field.drush.incW1yYW1Ccommands/core/help.drush.inciyYi&"commands/core/helpsingle.drush.inc:)yY:)0Bt@commands/core/image.drush.incyY܍Ucommands/core/init.drush.inc=yY=j%%!commands/core/locale.d8.drush.incyY%9commands/core/notify.drush.incyY⭶,commands/core/outputformat/csv_or_string.inc=yY=DZ#commands/core/outputformat/html.incyYXQ'commands/core/outputformat/html.tpl.phpjyYj3ζ#commands/core/outputformat/json.incyYR_(commands/core/outputformat/key_value.inc yY sJJ#commands/core/outputformat/list.inc yY ^&&commands/core/outputformat/message.incyYXa޶"commands/core/outputformat/php.inc$yY$uMt&commands/core/outputformat/print_r.incyY!7Ӷ%commands/core/outputformat/string.incyY?$commands/core/outputformat/table.incyY,commands/core/outputformat/topics/table.htmlyYXv )commands/core/outputformat/var_export.inc{yY{-+(commands/core/outputformat/variables.incyYI>#commands/core/outputformat/yaml.incyY$$commands/core/outputformat.drush.inc)EyY)Eicommands/core/queue.drush.inc_ yY_ Dcommands/core/role.drush.inc'yY'ɳcommands/core/rsync.core.inc90yY90v"ζcommands/core/scratch.phpyY8Xcommands/core/search.drush.incyY*"commands/core/shellalias.drush.incyY#zb$commands/core/site_install.drush.inc.yY.yc!commands/core/sitealias.drush.inc 7yY 7Zͤcommands/core/ssh.drush.incp yYp ?commands/core/state.drush.incyY9rcommands/core/topic.drush.inc@ yY@ aӶcommands/core/usage.drush.inc8yY8H commands/core/variable.drush.inc#yY#} commands/core/views.d8.drush.inc6yY6  commands/core/watchdog.drush.inc:yY:.>*ܶ(commands/make/generate.contents.make.inc/yY/avcommands/make/generate.make.inc'/yY'/}commands/make/lock.make.incyYͥp8commands/make/make.download.incHyYHⷜcommands/make/make.drush.incyY+>&׶commands/make/make.project.inc`yY`2ö commands/make/make.utilities.incRWyYRWsɆcommands/make/update.make.incy yYy }commands/pm/download.pm.incYByYYBݳ׶commands/pm/info.pm.inc|yY|D-commands/pm/package_handler/git_drupalorg.incq,yYq,$commands/pm/package_handler/wget.incmyYm]8Gcommands/pm/pm.drush.incAyYA̶commands/pm/projectinfo.pm.inc yY d~commands/pm/updatecode.pm.inc-AyY-A8BEcommands/pm/updatestatus.pm.inc#yY#gf&commands/pm/version_control/backup.incZ yYZ x*#commands/pm/version_control/bzr.incayYa>W#commands/pm/version_control/svn.incyY?qz#commands/runserver/d7-rs-router.php yY "Z#commands/runserver/d8-rs-router.php yY E^4(commands/runserver/runserver-prepend.php yY !" &commands/runserver/runserver.drush.incz#yYz#Uucommands/sql/sql.drush.incv`yYv`اcommands/sql/sqlsync.drush.inc 5yY 5b`commands/user/user.drush.inc<yY<ecommands/xh.drush.inc yY p$docs/bastion.mdyY`docs/bootstrap.mdyYl,edocs/commands.mdu/yYu/-docs/config-exporting.mdyY5Pdocs/context.md yY  docs/cron.md yY ;Sdocs/examples.md yY AĶ docs/index.md yY Nddocs/install-alternative.md| yY| M4docs/install.mdi yYi  docs/make.mdIyYIqrdocs/output-formats.mdyY%ⶶ docs/repl.mdsyYsA;docs/shellaliases.mdyYaM~Kdocs/shellscripts.md yY ,Ohdocs/strict-options.mdayYa^ # docs/usage.md2yY2gH[examples/drush.wrapperyY|5$examples/example.aliases.drushrc.php8yY8Y1examples/example.bashrc=yY= examples/example.drush.ini yY  ˜examples/example.drushrc.phpQ5yYQ5a_examples/example.make yY dexamples/example.make.yml yY examples/example.prompt.sh yY WӶexamples/git-bisect.example.shtyYt`oexamples/helloworld.scriptDyYDFוexamples/pm_update.drush.incKyYKؔ]examples/policy.drush.inc4yY4}\examples/sandwich-nocolor.txt yY sxexamples/sandwich-topic.txtyYexamples/sandwich.drush.inc*yY*ZUexamples/sandwich.txtyYEexamples/sync_enable.drush.incDyYDJn examples/sync_via_http.drush.incyY}examples/xkcd.drush.incVyYVow&includes/annotationcommand_adapter.incqyYqɪ7includes/array_column.incyY([includes/backend.incEyYE' includes/batch.inc yY m[includes/bootstrap.inc*MyY*M==includes/cache.inc yY YnDincludes/command.inc=yY=1Sincludes/complete.inc[yY[ 8includes/context.incYyYY^|includes/dbtng.incyYXincludes/drupal.incxyYxYΟincludes/drush.incNyYNJ2includes/engines.incQyYQֶincludes/environment.incfyYfincludes/exec.inc3yY3qoincludes/filesystem.inc^yY^Jincludes/output.incgyYgj3includes/preflight.incyY>includes/sitealias.incTyYTincludes/startup.incs<yYs<@%lib/Drush/Boot/BaseBoot.phpP yYP H߶lib/Drush/Boot/Boot.php^ yY^ J lib/Drush/Boot/DrupalBoot.phpUyYUh|lib/Drush/Boot/DrupalBoot6.php yY wʒlib/Drush/Boot/DrupalBoot7.php6 yY6 Vlib/Drush/Boot/DrupalBoot8.phpyY&lib/Drush/Boot/EmptyBoot.phpyY; "lib/Drush/Cache/CacheInterface.php yY &lib/Drush/Cache/FileCache.php yY '70`lib/Drush/Cache/JSONCache.phpyY/ƶ"lib/Drush/Command/Commandfiles.phpGyYG7m+lib/Drush/Command/CommandfilesInterface.phpyY'lib/Drush/Command/DrushInputAdapter.phpyYmᑶ(lib/Drush/Command/DrushOutputAdapter.php6yY6fZ(lib/Drush/Command/ServiceCommandlist.phpyYj@.lib/Drush/CommandFiles/core/browseCommands.phpH yYH 1lib/Drush/CommandFiles/core/DrupliconCommands.phpyY19-lib/Drush/CommandFiles/ExampleCommandFile.phpyY&?,lib/Drush/Commands/core/SanitizeCommands.php yY x1*lib/Drush/Commands/core/StatusCommands.phplyYl\v!lib/Drush/Drupal/DrupalKernel.phpyY)lib/Drush/Drupal/DrushServiceModifier.phpsyYs'lib/Drush/Drupal/ExtensionDiscovery.phpyYàќ-lib/Drush/Drupal/FindCommandsCompilerPass.php yY {lib/Drush/Log/DrushLog.php yY F\lib/Drush/Log/Logger.php{yY{plib/Drush/Log/LogLevel.phpyY#lib/Drush/Make/Parser/ParserIni.php yY I*)lib/Drush/Make/Parser/ParserInterface.phpyY$lib/Drush/Make/Parser/ParserYaml.phpyYv#lib/Drush/Psysh/Caster.php yY   lib/Drush/Psysh/DrushCommand.phpyYت$lib/Drush/Psysh/DrushHelpCommand.php yY k}lib/Drush/Psysh/Shell.php`yY`tŹlib/Drush/Queue/Queue6.phpmyYmx`lib/Drush/Queue/Queue7.phpyYࢹUlib/Drush/Queue/Queue8.phpyYֹ*lib/Drush/Queue/QueueBase.php1yY1%R "lib/Drush/Queue/QueueException.phpJyYJ&g"lib/Drush/Queue/QueueInterface.phpxyYxK\5lib/Drush/Role/Role6.phpyYY,lib/Drush/Role/Role7.phpFyYFdulib/Drush/Role/Role8.phpmyYmlib/Drush/Role/RoleBase.php$ yY$ H lib/Drush/Role/RoleException.phpHyYHpRɶlib/Drush/Sql/Sql6.phpyYv˦lib/Drush/Sql/Sql7.phpyY?*lib/Drush/Sql/Sql8.phpyYfm4lib/Drush/Sql/SqlBase.php/yY/4Hɶlib/Drush/Sql/SqlException.phpFyYF,񝙶lib/Drush/Sql/Sqlmysql.phpyY lib/Drush/Sql/Sqloracle.php yY ,jlib/Drush/Sql/Sqlpgsql.phpyYF`}lib/Drush/Sql/Sqlsqlite.phpA yYA hlib/Drush/Sql/Sqlsqlsrv.php yY i{lib/Drush/Sql/SqlVersion.phpyYw#lib/Drush/UpdateService/Project.phpSyYS d'lib/Drush/UpdateService/ReleaseInfo.phpyY-lib/Drush/UpdateService/StatusInfoDrupal6.phpyYPyP-lib/Drush/UpdateService/StatusInfoDrupal7.php? yY? w=-lib/Drush/UpdateService/StatusInfoDrupal8.phpyY}sE+lib/Drush/UpdateService/StatusInfoDrush.phpL>yYL>_>/lib/Drush/UpdateService/StatusInfoInterface.phpyYU(zlib/Drush/User/User6.phpyYЏrlib/Drush/User/User7.phpyYRlib/Drush/User/User8.phpyYalib/Drush/User/UserList.phpyY,Z$lib/Drush/User/UserListException.phpLyYLl]lib/Drush/User/UserSingle6.phpyYlib/Drush/User/UserSingle7.phpYyYYilib/Drush/User/UserSingle8.phpLyYLn2O!lib/Drush/User/UserSingleBase.phpG yYG (lib/Drush/User/UserVersion.php|yY|X{misc/druplicon-color.txtyYWJmisc/druplicon-no_color.txtbyYb5'misc/windrush_build/assets/composer.batjyYj%Ŷ$misc/windrush_build/assets/drush.batyYg@0misc/windrush_build/assets/notify_env_change.exeyY Ŷ%misc/windrush_build/assets/setenv.bat(yY(x?]$misc/windrush_build/assets/setenv.js{yY{Fmisc/windrush_build/README.mdQyYQ=+X"misc/windrush_build/windrush_buildB yYB *;  drush.api.php6yY6˅0drush.complete.shyY drush.infoyY drush.launcherZyYZRQ drush.phpyYD1drush_logo-black.pngZyYZ!KҶ README.mdg yYg { =drushyYyd $vendorDir . '/pear/console_table/Table.php', 'File_Iterator' => $vendorDir . '/phpunit/php-file-iterator/src/Iterator.php', 'File_Iterator_Facade' => $vendorDir . '/phpunit/php-file-iterator/src/Facade.php', 'File_Iterator_Factory' => $vendorDir . '/phpunit/php-file-iterator/src/Factory.php', 'PHPUnit_Exception' => $vendorDir . '/phpunit/phpunit/src/Exception.php', 'PHPUnit_Extensions_GroupTestSuite' => $vendorDir . '/phpunit/phpunit/src/Extensions/GroupTestSuite.php', 'PHPUnit_Extensions_PhptTestCase' => $vendorDir . '/phpunit/phpunit/src/Extensions/PhptTestCase.php', 'PHPUnit_Extensions_PhptTestSuite' => $vendorDir . '/phpunit/phpunit/src/Extensions/PhptTestSuite.php', 'PHPUnit_Extensions_RepeatedTest' => $vendorDir . '/phpunit/phpunit/src/Extensions/RepeatedTest.php', 'PHPUnit_Extensions_TestDecorator' => $vendorDir . '/phpunit/phpunit/src/Extensions/TestDecorator.php', 'PHPUnit_Extensions_TicketListener' => $vendorDir . '/phpunit/phpunit/src/Extensions/TicketListener.php', 'PHPUnit_Framework_Assert' => $vendorDir . '/phpunit/phpunit/src/Framework/Assert.php', 'PHPUnit_Framework_AssertionFailedError' => $vendorDir . '/phpunit/phpunit/src/Framework/AssertionFailedError.php', 'PHPUnit_Framework_BaseTestListener' => $vendorDir . '/phpunit/phpunit/src/Framework/BaseTestListener.php', 'PHPUnit_Framework_CodeCoverageException' => $vendorDir . '/phpunit/phpunit/src/Framework/CodeCoverageException.php', 'PHPUnit_Framework_Constraint' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint.php', 'PHPUnit_Framework_Constraint_And' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/And.php', 'PHPUnit_Framework_Constraint_ArrayHasKey' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ArrayHasKey.php', 'PHPUnit_Framework_Constraint_ArraySubset' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ArraySubset.php', 'PHPUnit_Framework_Constraint_Attribute' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Attribute.php', 'PHPUnit_Framework_Constraint_Callback' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Callback.php', 'PHPUnit_Framework_Constraint_ClassHasAttribute' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ClassHasAttribute.php', 'PHPUnit_Framework_Constraint_ClassHasStaticAttribute' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ClassHasStaticAttribute.php', 'PHPUnit_Framework_Constraint_Composite' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Composite.php', 'PHPUnit_Framework_Constraint_Count' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Count.php', 'PHPUnit_Framework_Constraint_Exception' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Exception.php', 'PHPUnit_Framework_Constraint_ExceptionCode' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ExceptionCode.php', 'PHPUnit_Framework_Constraint_ExceptionMessage' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ExceptionMessage.php', 'PHPUnit_Framework_Constraint_ExceptionMessageRegExp' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ExceptionMessageRegExp.php', 'PHPUnit_Framework_Constraint_FileExists' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/FileExists.php', 'PHPUnit_Framework_Constraint_GreaterThan' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/GreaterThan.php', 'PHPUnit_Framework_Constraint_IsAnything' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsAnything.php', 'PHPUnit_Framework_Constraint_IsEmpty' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsEmpty.php', 'PHPUnit_Framework_Constraint_IsEqual' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsEqual.php', 'PHPUnit_Framework_Constraint_IsFalse' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsFalse.php', 'PHPUnit_Framework_Constraint_IsIdentical' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php', 'PHPUnit_Framework_Constraint_IsInstanceOf' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsInstanceOf.php', 'PHPUnit_Framework_Constraint_IsJson' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsJson.php', 'PHPUnit_Framework_Constraint_IsNull' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsNull.php', 'PHPUnit_Framework_Constraint_IsTrue' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsTrue.php', 'PHPUnit_Framework_Constraint_IsType' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/IsType.php', 'PHPUnit_Framework_Constraint_JsonMatches' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/JsonMatches.php', 'PHPUnit_Framework_Constraint_JsonMatches_ErrorMessageProvider' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/JsonMatches/ErrorMessageProvider.php', 'PHPUnit_Framework_Constraint_LessThan' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/LessThan.php', 'PHPUnit_Framework_Constraint_Not' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Not.php', 'PHPUnit_Framework_Constraint_ObjectHasAttribute' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/ObjectHasAttribute.php', 'PHPUnit_Framework_Constraint_Or' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Or.php', 'PHPUnit_Framework_Constraint_PCREMatch' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/PCREMatch.php', 'PHPUnit_Framework_Constraint_SameSize' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/SameSize.php', 'PHPUnit_Framework_Constraint_StringContains' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/StringContains.php', 'PHPUnit_Framework_Constraint_StringEndsWith' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/StringEndsWith.php', 'PHPUnit_Framework_Constraint_StringMatches' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/StringMatches.php', 'PHPUnit_Framework_Constraint_StringStartsWith' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/StringStartsWith.php', 'PHPUnit_Framework_Constraint_TraversableContains' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/TraversableContains.php', 'PHPUnit_Framework_Constraint_TraversableContainsOnly' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/TraversableContainsOnly.php', 'PHPUnit_Framework_Constraint_Xor' => $vendorDir . '/phpunit/phpunit/src/Framework/Constraint/Xor.php', 'PHPUnit_Framework_Error' => $vendorDir . '/phpunit/phpunit/src/Framework/Error.php', 'PHPUnit_Framework_Error_Deprecated' => $vendorDir . '/phpunit/phpunit/src/Framework/Error/Deprecated.php', 'PHPUnit_Framework_Error_Notice' => $vendorDir . '/phpunit/phpunit/src/Framework/Error/Notice.php', 'PHPUnit_Framework_Error_Warning' => $vendorDir . '/phpunit/phpunit/src/Framework/Error/Warning.php', 'PHPUnit_Framework_Exception' => $vendorDir . '/phpunit/phpunit/src/Framework/Exception.php', 'PHPUnit_Framework_ExceptionWrapper' => $vendorDir . '/phpunit/phpunit/src/Framework/ExceptionWrapper.php', 'PHPUnit_Framework_ExpectationFailedException' => $vendorDir . '/phpunit/phpunit/src/Framework/ExpectationFailedException.php', 'PHPUnit_Framework_IncompleteTest' => $vendorDir . '/phpunit/phpunit/src/Framework/IncompleteTest.php', 'PHPUnit_Framework_IncompleteTestCase' => $vendorDir . '/phpunit/phpunit/src/Framework/IncompleteTestCase.php', 'PHPUnit_Framework_IncompleteTestError' => $vendorDir . '/phpunit/phpunit/src/Framework/IncompleteTestError.php', 'PHPUnit_Framework_InvalidCoversTargetError' => $vendorDir . '/phpunit/phpunit/src/Framework/InvalidCoversTargetError.php', 'PHPUnit_Framework_InvalidCoversTargetException' => $vendorDir . '/phpunit/phpunit/src/Framework/InvalidCoversTargetException.php', 'PHPUnit_Framework_MockObject_BadMethodCallException' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/BadMethodCallException.php', 'PHPUnit_Framework_MockObject_Builder_Identity' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Identity.php', 'PHPUnit_Framework_MockObject_Builder_InvocationMocker' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/InvocationMocker.php', 'PHPUnit_Framework_MockObject_Builder_Match' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Match.php', 'PHPUnit_Framework_MockObject_Builder_MethodNameMatch' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/MethodNameMatch.php', 'PHPUnit_Framework_MockObject_Builder_Namespace' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Namespace.php', 'PHPUnit_Framework_MockObject_Builder_ParametersMatch' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/ParametersMatch.php', 'PHPUnit_Framework_MockObject_Builder_Stub' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Stub.php', 'PHPUnit_Framework_MockObject_Exception' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/Exception.php', 'PHPUnit_Framework_MockObject_Generator' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator.php', 'PHPUnit_Framework_MockObject_Invocation' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation.php', 'PHPUnit_Framework_MockObject_InvocationMocker' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/InvocationMocker.php', 'PHPUnit_Framework_MockObject_Invocation_Object' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Object.php', 'PHPUnit_Framework_MockObject_Invocation_Static' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Static.php', 'PHPUnit_Framework_MockObject_Invokable' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invokable.php', 'PHPUnit_Framework_MockObject_Matcher' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher.php', 'PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyInvokedCount.php', 'PHPUnit_Framework_MockObject_Matcher_AnyParameters' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyParameters.php', 'PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/ConsecutiveParameters.php', 'PHPUnit_Framework_MockObject_Matcher_Invocation' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Invocation.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtIndex.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastCount' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastOnce.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtMostCount' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtMostCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedCount' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedRecorder' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedRecorder.php', 'PHPUnit_Framework_MockObject_Matcher_MethodName' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/MethodName.php', 'PHPUnit_Framework_MockObject_Matcher_Parameters' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Parameters.php', 'PHPUnit_Framework_MockObject_Matcher_StatelessInvocation' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/StatelessInvocation.php', 'PHPUnit_Framework_MockObject_MockBuilder' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockBuilder.php', 'PHPUnit_Framework_MockObject_MockObject' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockObject.php', 'PHPUnit_Framework_MockObject_RuntimeException' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/RuntimeException.php', 'PHPUnit_Framework_MockObject_Stub' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub.php', 'PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ConsecutiveCalls.php', 'PHPUnit_Framework_MockObject_Stub_Exception' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Exception.php', 'PHPUnit_Framework_MockObject_Stub_MatcherCollection' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/MatcherCollection.php', 'PHPUnit_Framework_MockObject_Stub_Return' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Return.php', 'PHPUnit_Framework_MockObject_Stub_ReturnArgument' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnArgument.php', 'PHPUnit_Framework_MockObject_Stub_ReturnCallback' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnCallback.php', 'PHPUnit_Framework_MockObject_Stub_ReturnSelf' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnSelf.php', 'PHPUnit_Framework_MockObject_Stub_ReturnValueMap' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnValueMap.php', 'PHPUnit_Framework_MockObject_Verifiable' => $vendorDir . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Verifiable.php', 'PHPUnit_Framework_OutputError' => $vendorDir . '/phpunit/phpunit/src/Framework/OutputError.php', 'PHPUnit_Framework_RiskyTest' => $vendorDir . '/phpunit/phpunit/src/Framework/RiskyTest.php', 'PHPUnit_Framework_RiskyTestError' => $vendorDir . '/phpunit/phpunit/src/Framework/RiskyTestError.php', 'PHPUnit_Framework_SelfDescribing' => $vendorDir . '/phpunit/phpunit/src/Framework/SelfDescribing.php', 'PHPUnit_Framework_SkippedTest' => $vendorDir . '/phpunit/phpunit/src/Framework/SkippedTest.php', 'PHPUnit_Framework_SkippedTestCase' => $vendorDir . '/phpunit/phpunit/src/Framework/SkippedTestCase.php', 'PHPUnit_Framework_SkippedTestError' => $vendorDir . '/phpunit/phpunit/src/Framework/SkippedTestError.php', 'PHPUnit_Framework_SkippedTestSuiteError' => $vendorDir . '/phpunit/phpunit/src/Framework/SkippedTestSuiteError.php', 'PHPUnit_Framework_SyntheticError' => $vendorDir . '/phpunit/phpunit/src/Framework/SyntheticError.php', 'PHPUnit_Framework_Test' => $vendorDir . '/phpunit/phpunit/src/Framework/Test.php', 'PHPUnit_Framework_TestCase' => $vendorDir . '/phpunit/phpunit/src/Framework/TestCase.php', 'PHPUnit_Framework_TestFailure' => $vendorDir . '/phpunit/phpunit/src/Framework/TestFailure.php', 'PHPUnit_Framework_TestListener' => $vendorDir . '/phpunit/phpunit/src/Framework/TestListener.php', 'PHPUnit_Framework_TestResult' => $vendorDir . '/phpunit/phpunit/src/Framework/TestResult.php', 'PHPUnit_Framework_TestSuite' => $vendorDir . '/phpunit/phpunit/src/Framework/TestSuite.php', 'PHPUnit_Framework_TestSuite_DataProvider' => $vendorDir . '/phpunit/phpunit/src/Framework/TestSuite/DataProvider.php', 'PHPUnit_Framework_UnintentionallyCoveredCodeError' => $vendorDir . '/phpunit/phpunit/src/Framework/UnintentionallyCoveredCodeError.php', 'PHPUnit_Framework_Warning' => $vendorDir . '/phpunit/phpunit/src/Framework/Warning.php', 'PHPUnit_Runner_BaseTestRunner' => $vendorDir . '/phpunit/phpunit/src/Runner/BaseTestRunner.php', 'PHPUnit_Runner_Exception' => $vendorDir . '/phpunit/phpunit/src/Runner/Exception.php', 'PHPUnit_Runner_Filter_Factory' => $vendorDir . '/phpunit/phpunit/src/Runner/Filter/Factory.php', 'PHPUnit_Runner_Filter_GroupFilterIterator' => $vendorDir . '/phpunit/phpunit/src/Runner/Filter/Group.php', 'PHPUnit_Runner_Filter_Group_Exclude' => $vendorDir . '/phpunit/phpunit/src/Runner/Filter/Group/Exclude.php', 'PHPUnit_Runner_Filter_Group_Include' => $vendorDir . '/phpunit/phpunit/src/Runner/Filter/Group/Include.php', 'PHPUnit_Runner_Filter_Test' => $vendorDir . '/phpunit/phpunit/src/Runner/Filter/Test.php', 'PHPUnit_Runner_StandardTestSuiteLoader' => $vendorDir . '/phpunit/phpunit/src/Runner/StandardTestSuiteLoader.php', 'PHPUnit_Runner_TestSuiteLoader' => $vendorDir . '/phpunit/phpunit/src/Runner/TestSuiteLoader.php', 'PHPUnit_Runner_Version' => $vendorDir . '/phpunit/phpunit/src/Runner/Version.php', 'PHPUnit_TextUI_Command' => $vendorDir . '/phpunit/phpunit/src/TextUI/Command.php', 'PHPUnit_TextUI_ResultPrinter' => $vendorDir . '/phpunit/phpunit/src/TextUI/ResultPrinter.php', 'PHPUnit_TextUI_TestRunner' => $vendorDir . '/phpunit/phpunit/src/TextUI/TestRunner.php', 'PHPUnit_Util_Blacklist' => $vendorDir . '/phpunit/phpunit/src/Util/Blacklist.php', 'PHPUnit_Util_Configuration' => $vendorDir . '/phpunit/phpunit/src/Util/Configuration.php', 'PHPUnit_Util_ErrorHandler' => $vendorDir . '/phpunit/phpunit/src/Util/ErrorHandler.php', 'PHPUnit_Util_Fileloader' => $vendorDir . '/phpunit/phpunit/src/Util/Fileloader.php', 'PHPUnit_Util_Filesystem' => $vendorDir . '/phpunit/phpunit/src/Util/Filesystem.php', 'PHPUnit_Util_Filter' => $vendorDir . '/phpunit/phpunit/src/Util/Filter.php', 'PHPUnit_Util_Getopt' => $vendorDir . '/phpunit/phpunit/src/Util/Getopt.php', 'PHPUnit_Util_GlobalState' => $vendorDir . '/phpunit/phpunit/src/Util/GlobalState.php', 'PHPUnit_Util_InvalidArgumentHelper' => $vendorDir . '/phpunit/phpunit/src/Util/InvalidArgumentHelper.php', 'PHPUnit_Util_Log_JSON' => $vendorDir . '/phpunit/phpunit/src/Util/Log/JSON.php', 'PHPUnit_Util_Log_JUnit' => $vendorDir . '/phpunit/phpunit/src/Util/Log/JUnit.php', 'PHPUnit_Util_Log_TAP' => $vendorDir . '/phpunit/phpunit/src/Util/Log/TAP.php', 'PHPUnit_Util_PHP' => $vendorDir . '/phpunit/phpunit/src/Util/PHP.php', 'PHPUnit_Util_PHP_Default' => $vendorDir . '/phpunit/phpunit/src/Util/PHP/Default.php', 'PHPUnit_Util_PHP_Windows' => $vendorDir . '/phpunit/phpunit/src/Util/PHP/Windows.php', 'PHPUnit_Util_Printer' => $vendorDir . '/phpunit/phpunit/src/Util/Printer.php', 'PHPUnit_Util_Regex' => $vendorDir . '/phpunit/phpunit/src/Util/Regex.php', 'PHPUnit_Util_String' => $vendorDir . '/phpunit/phpunit/src/Util/String.php', 'PHPUnit_Util_Test' => $vendorDir . '/phpunit/phpunit/src/Util/Test.php', 'PHPUnit_Util_TestDox_NamePrettifier' => $vendorDir . '/phpunit/phpunit/src/Util/TestDox/NamePrettifier.php', 'PHPUnit_Util_TestDox_ResultPrinter' => $vendorDir . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter.php', 'PHPUnit_Util_TestDox_ResultPrinter_HTML' => $vendorDir . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter/HTML.php', 'PHPUnit_Util_TestDox_ResultPrinter_Text' => $vendorDir . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter/Text.php', 'PHPUnit_Util_TestSuiteIterator' => $vendorDir . '/phpunit/phpunit/src/Util/TestSuiteIterator.php', 'PHPUnit_Util_Type' => $vendorDir . '/phpunit/phpunit/src/Util/Type.php', 'PHPUnit_Util_XML' => $vendorDir . '/phpunit/phpunit/src/Util/XML.php', 'PHP_CodeCoverage' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage.php', 'PHP_CodeCoverage_Driver' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Driver.php', 'PHP_CodeCoverage_Driver_HHVM' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Driver/HHVM.php', 'PHP_CodeCoverage_Driver_PHPDBG' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Driver/PHPDBG.php', 'PHP_CodeCoverage_Driver_Xdebug' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Driver/Xdebug.php', 'PHP_CodeCoverage_Exception' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Exception.php', 'PHP_CodeCoverage_Exception_UnintentionallyCoveredCode' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Exception/UnintentionallyCoveredCode.php', 'PHP_CodeCoverage_Filter' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Filter.php', 'PHP_CodeCoverage_Report_Clover' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Clover.php', 'PHP_CodeCoverage_Report_Crap4j' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Crap4j.php', 'PHP_CodeCoverage_Report_Factory' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Factory.php', 'PHP_CodeCoverage_Report_HTML' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML.php', 'PHP_CodeCoverage_Report_HTML_Renderer' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer.php', 'PHP_CodeCoverage_Report_HTML_Renderer_Dashboard' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php', 'PHP_CodeCoverage_Report_HTML_Renderer_Directory' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/Directory.php', 'PHP_CodeCoverage_Report_HTML_Renderer_File' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/File.php', 'PHP_CodeCoverage_Report_Node' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Node.php', 'PHP_CodeCoverage_Report_Node_Directory' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Node/Directory.php', 'PHP_CodeCoverage_Report_Node_File' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Node/File.php', 'PHP_CodeCoverage_Report_Node_Iterator' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Node/Iterator.php', 'PHP_CodeCoverage_Report_PHP' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/PHP.php', 'PHP_CodeCoverage_Report_Text' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Text.php', 'PHP_CodeCoverage_Report_XML' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML.php', 'PHP_CodeCoverage_Report_XML_Directory' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Directory.php', 'PHP_CodeCoverage_Report_XML_File' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File.php', 'PHP_CodeCoverage_Report_XML_File_Coverage' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Coverage.php', 'PHP_CodeCoverage_Report_XML_File_Method' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Method.php', 'PHP_CodeCoverage_Report_XML_File_Report' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Report.php', 'PHP_CodeCoverage_Report_XML_File_Unit' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Unit.php', 'PHP_CodeCoverage_Report_XML_Node' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Node.php', 'PHP_CodeCoverage_Report_XML_Project' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Project.php', 'PHP_CodeCoverage_Report_XML_Tests' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Tests.php', 'PHP_CodeCoverage_Report_XML_Totals' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Totals.php', 'PHP_CodeCoverage_Util' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Util.php', 'PHP_CodeCoverage_Util_InvalidArgumentHelper' => $vendorDir . '/phpunit/php-code-coverage/src/CodeCoverage/Util/InvalidArgumentHelper.php', 'PHP_Timer' => $vendorDir . '/phpunit/php-timer/src/Timer.php', 'PHP_Token' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_TokenWithScope' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_TokenWithScopeAndVisibility' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ABSTRACT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AMPERSAND' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AND_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ARRAY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ARRAY_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ASYNC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AWAIT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BACKTICK' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BAD_CHARACTER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOLEAN_AND' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOLEAN_OR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOL_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BREAK' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CALLABLE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CARET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CASE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CATCH' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CHARACTER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS_C' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS_NAME_CONSTANT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLONE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_BRACKET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_CURLY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_SQUARE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_TAG' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COALESCE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COLON' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMMA' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMMENT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMPILER_HALT_OFFSET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONCAT_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONSTANT_ENCAPSED_STRING' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONTINUE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CURLY_OPEN' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DEC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DECLARE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DEFAULT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIV' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIV_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DNUMBER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DO' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOC_COMMENT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOLLAR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOLLAR_OPEN_CURLY_BRACES' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_ARROW' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_COLON' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_QUOTES' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ECHO' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELLIPSIS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELSE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELSEIF' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EMPTY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENCAPSED_AND_WHITESPACE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDDECLARE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDFOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDFOREACH' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDIF' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDSWITCH' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDWHILE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_END_HEREDOC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENUM' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EQUALS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EVAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXCLAMATION_MARK' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXIT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXTENDS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FILE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FINAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FINALLY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FOREACH' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FUNCTION' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FUNC_C' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GLOBAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GOTO' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_HALT_COMPILER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IF' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IMPLEMENTS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IN' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INCLUDE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INCLUDE_ONCE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INLINE_HTML' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INSTANCEOF' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INSTEADOF' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INTERFACE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INT_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ISSET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_GREATER_OR_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_IDENTICAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_NOT_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_NOT_IDENTICAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_SMALLER_OR_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_Includes' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_JOIN' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_ARROW' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_CP' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_OP' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LINE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LIST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LNUMBER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_AND' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_OR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_XOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_METHOD_C' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MINUS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MINUS_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MOD_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MULT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MUL_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NAMESPACE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NEW' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NS_C' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NS_SEPARATOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NUM_STRING' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OBJECT_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OBJECT_OPERATOR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ONUMBER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_BRACKET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_CURLY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_SQUARE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_TAG' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_TAG_WITH_ECHO' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OR_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PAAMAYIM_NEKUDOTAYIM' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PERCENT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PIPE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PLUS' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PLUS_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_POW' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_POW_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PRINT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PRIVATE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PROTECTED' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PUBLIC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_QUESTION_MARK' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_REQUIRE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_REQUIRE_ONCE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_RETURN' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SEMICOLON' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SHAPE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SL_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SPACESHIP' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SR_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_START_HEREDOC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STATIC' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING_VARNAME' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SUPER' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SWITCH' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_Stream' => $vendorDir . '/phpunit/php-token-stream/src/Token/Stream.php', 'PHP_Token_Stream_CachingFactory' => $vendorDir . '/phpunit/php-token-stream/src/Token/Stream/CachingFactory.php', 'PHP_Token_THROW' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TILDE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRAIT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRAIT_C' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPELIST_GT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPELIST_LT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_UNSET' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_UNSET_CAST' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_USE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_USE_FUNCTION' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_VAR' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_VARIABLE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHERE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHILE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHITESPACE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_ATTRIBUTE' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CATEGORY' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CATEGORY_LABEL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CHILDREN' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_LABEL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_REQUIRED' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TAG_GT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TAG_LT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TEXT' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XOR_EQUAL' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_YIELD' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_YIELD_FROM' => $vendorDir . '/phpunit/php-token-stream/src/Token.php', 'SebastianBergmann\\Comparator\\ArrayComparator' => $vendorDir . '/sebastian/comparator/src/ArrayComparator.php', 'SebastianBergmann\\Comparator\\Comparator' => $vendorDir . '/sebastian/comparator/src/Comparator.php', 'SebastianBergmann\\Comparator\\ComparisonFailure' => $vendorDir . '/sebastian/comparator/src/ComparisonFailure.php', 'SebastianBergmann\\Comparator\\DOMNodeComparator' => $vendorDir . '/sebastian/comparator/src/DOMNodeComparator.php', 'SebastianBergmann\\Comparator\\DateTimeComparator' => $vendorDir . '/sebastian/comparator/src/DateTimeComparator.php', 'SebastianBergmann\\Comparator\\DoubleComparator' => $vendorDir . '/sebastian/comparator/src/DoubleComparator.php', 'SebastianBergmann\\Comparator\\ExceptionComparator' => $vendorDir . '/sebastian/comparator/src/ExceptionComparator.php', 'SebastianBergmann\\Comparator\\Factory' => $vendorDir . '/sebastian/comparator/src/Factory.php', 'SebastianBergmann\\Comparator\\MockObjectComparator' => $vendorDir . '/sebastian/comparator/src/MockObjectComparator.php', 'SebastianBergmann\\Comparator\\NumericComparator' => $vendorDir . '/sebastian/comparator/src/NumericComparator.php', 'SebastianBergmann\\Comparator\\ObjectComparator' => $vendorDir . '/sebastian/comparator/src/ObjectComparator.php', 'SebastianBergmann\\Comparator\\ResourceComparator' => $vendorDir . '/sebastian/comparator/src/ResourceComparator.php', 'SebastianBergmann\\Comparator\\ScalarComparator' => $vendorDir . '/sebastian/comparator/src/ScalarComparator.php', 'SebastianBergmann\\Comparator\\SplObjectStorageComparator' => $vendorDir . '/sebastian/comparator/src/SplObjectStorageComparator.php', 'SebastianBergmann\\Comparator\\TypeComparator' => $vendorDir . '/sebastian/comparator/src/TypeComparator.php', 'SebastianBergmann\\Diff\\Chunk' => $vendorDir . '/sebastian/diff/src/Chunk.php', 'SebastianBergmann\\Diff\\Diff' => $vendorDir . '/sebastian/diff/src/Diff.php', 'SebastianBergmann\\Diff\\Differ' => $vendorDir . '/sebastian/diff/src/Differ.php', 'SebastianBergmann\\Diff\\LCS\\LongestCommonSubsequence' => $vendorDir . '/sebastian/diff/src/LCS/LongestCommonSubsequence.php', 'SebastianBergmann\\Diff\\LCS\\MemoryEfficientImplementation' => $vendorDir . '/sebastian/diff/src/LCS/MemoryEfficientLongestCommonSubsequenceImplementation.php', 'SebastianBergmann\\Diff\\LCS\\TimeEfficientImplementation' => $vendorDir . '/sebastian/diff/src/LCS/TimeEfficientLongestCommonSubsequenceImplementation.php', 'SebastianBergmann\\Diff\\Line' => $vendorDir . '/sebastian/diff/src/Line.php', 'SebastianBergmann\\Diff\\Parser' => $vendorDir . '/sebastian/diff/src/Parser.php', 'SebastianBergmann\\Environment\\Console' => $vendorDir . '/sebastian/environment/src/Console.php', 'SebastianBergmann\\Environment\\Runtime' => $vendorDir . '/sebastian/environment/src/Runtime.php', 'SebastianBergmann\\Exporter\\Exporter' => $vendorDir . '/sebastian/exporter/src/Exporter.php', 'SebastianBergmann\\GlobalState\\Blacklist' => $vendorDir . '/sebastian/global-state/src/Blacklist.php', 'SebastianBergmann\\GlobalState\\CodeExporter' => $vendorDir . '/sebastian/global-state/src/CodeExporter.php', 'SebastianBergmann\\GlobalState\\Exception' => $vendorDir . '/sebastian/global-state/src/Exception.php', 'SebastianBergmann\\GlobalState\\Restorer' => $vendorDir . '/sebastian/global-state/src/Restorer.php', 'SebastianBergmann\\GlobalState\\RuntimeException' => $vendorDir . '/sebastian/global-state/src/RuntimeException.php', 'SebastianBergmann\\GlobalState\\Snapshot' => $vendorDir . '/sebastian/global-state/src/Snapshot.php', 'SebastianBergmann\\RecursionContext\\Context' => $vendorDir . '/sebastian/recursion-context/src/Context.php', 'SebastianBergmann\\RecursionContext\\Exception' => $vendorDir . '/sebastian/recursion-context/src/Exception.php', 'SebastianBergmann\\RecursionContext\\InvalidArgumentException' => $vendorDir . '/sebastian/recursion-context/src/InvalidArgumentException.php', 'SebastianBergmann\\Version' => $vendorDir . '/sebastian/version/src/Version.php', 'Text_Template' => $vendorDir . '/phpunit/php-text-template/src/Template.php', ); $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', 'e7223560d890eab89cda23685e711e2c' => $vendorDir . '/psy/psysh/src/Psy/functions.php', ); array($vendorDir . '/phpdocumentor/reflection-docblock/src'), 'Unish' => array($baseDir . '/tests'), 'Prophecy\\' => array($vendorDir . '/phpspec/prophecy/src'), 'PHPDocsMD' => array($vendorDir . '/victorjonsson/markdowndocs/src'), 'JakubOnderka\\PhpConsoleHighlighter' => array($vendorDir . '/jakub-onderka/php-console-highlighter/src'), 'JakubOnderka\\PhpConsoleColor' => array($vendorDir . '/jakub-onderka/php-console-color/src'), 'Drush' => array($baseDir . '/lib'), 'Consolidation' => array($baseDir . '/lib'), ); array($vendorDir . '/dnoegel/php-xdg-base-dir/src'), 'Webmozart\\PathUtil\\' => array($vendorDir . '/webmozart/path-util/src'), 'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Component\\Yaml\\' => array($vendorDir . '/symfony/yaml'), 'Symfony\\Component\\VarDumper\\' => array($vendorDir . '/symfony/var-dumper'), 'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'), 'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'), 'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'), 'Symfony\\Component\\Debug\\' => array($vendorDir . '/symfony/debug'), 'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'), 'Psy\\' => array($vendorDir . '/psy/psysh/src/Psy'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'Doctrine\\Instantiator\\' => array($vendorDir . '/doctrine/instantiator/src/Doctrine/Instantiator'), 'Consolidation\\OutputFormatters\\' => array($vendorDir . '/consolidation/output-formatters/src'), 'Consolidation\\AnnotatedCommand\\' => array($vendorDir . '/consolidation/annotated-command/src'), ); = 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit13c106add9df9044427bf251f433f228::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } $loader->register(true); if ($useStaticLoader) { $includeFiles = Composer\Autoload\ComposerStaticInit13c106add9df9044427bf251f433f228::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { composerRequire13c106add9df9044427bf251f433f228($fileIdentifier, $file); } return $loader; } } function composerRequire13c106add9df9044427bf251f433f228($fileIdentifier, $file) { if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { require $file; $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; } } __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', 'e7223560d890eab89cda23685e711e2c' => __DIR__ . '/..' . '/psy/psysh/src/Psy/functions.php', ); public static $prefixLengthsPsr4 = array ( 'X' => array ( 'XdgBaseDir\\' => 11, ), 'W' => array ( 'Webmozart\\PathUtil\\' => 19, 'Webmozart\\Assert\\' => 17, ), 'S' => array ( 'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Component\\Yaml\\' => 23, 'Symfony\\Component\\VarDumper\\' => 28, 'Symfony\\Component\\Process\\' => 26, 'Symfony\\Component\\Finder\\' => 25, 'Symfony\\Component\\EventDispatcher\\' => 34, 'Symfony\\Component\\Debug\\' => 24, 'Symfony\\Component\\Console\\' => 26, ), 'P' => array ( 'Psy\\' => 4, 'Psr\\Log\\' => 8, 'PhpParser\\' => 10, ), 'D' => array ( 'Doctrine\\Instantiator\\' => 22, ), 'C' => array ( 'Consolidation\\OutputFormatters\\' => 31, 'Consolidation\\AnnotatedCommand\\' => 31, ), ); public static $prefixDirsPsr4 = array ( 'XdgBaseDir\\' => array ( 0 => __DIR__ . '/..' . '/dnoegel/php-xdg-base-dir/src', ), 'Webmozart\\PathUtil\\' => array ( 0 => __DIR__ . '/..' . '/webmozart/path-util/src', ), 'Webmozart\\Assert\\' => array ( 0 => __DIR__ . '/..' . '/webmozart/assert/src', ), 'Symfony\\Polyfill\\Mbstring\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', ), 'Symfony\\Component\\Yaml\\' => array ( 0 => __DIR__ . '/..' . '/symfony/yaml', ), 'Symfony\\Component\\VarDumper\\' => array ( 0 => __DIR__ . '/..' . '/symfony/var-dumper', ), 'Symfony\\Component\\Process\\' => array ( 0 => __DIR__ . '/..' . '/symfony/process', ), 'Symfony\\Component\\Finder\\' => array ( 0 => __DIR__ . '/..' . '/symfony/finder', ), 'Symfony\\Component\\EventDispatcher\\' => array ( 0 => __DIR__ . '/..' . '/symfony/event-dispatcher', ), 'Symfony\\Component\\Debug\\' => array ( 0 => __DIR__ . '/..' . '/symfony/debug', ), 'Symfony\\Component\\Console\\' => array ( 0 => __DIR__ . '/..' . '/symfony/console', ), 'Psy\\' => array ( 0 => __DIR__ . '/..' . '/psy/psysh/src/Psy', ), 'Psr\\Log\\' => array ( 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', ), 'PhpParser\\' => array ( 0 => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser', ), 'Doctrine\\Instantiator\\' => array ( 0 => __DIR__ . '/..' . '/doctrine/instantiator/src/Doctrine/Instantiator', ), 'Consolidation\\OutputFormatters\\' => array ( 0 => __DIR__ . '/..' . '/consolidation/output-formatters/src', ), 'Consolidation\\AnnotatedCommand\\' => array ( 0 => __DIR__ . '/..' . '/consolidation/annotated-command/src', ), ); public static $prefixesPsr0 = array ( 'p' => array ( 'phpDocumentor' => array ( 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src', ), ), 'U' => array ( 'Unish' => array ( 0 => __DIR__ . '/../..' . '/tests', ), ), 'P' => array ( 'Prophecy\\' => array ( 0 => __DIR__ . '/..' . '/phpspec/prophecy/src', ), 'PHPDocsMD' => array ( 0 => __DIR__ . '/..' . '/victorjonsson/markdowndocs/src', ), ), 'J' => array ( 'JakubOnderka\\PhpConsoleHighlighter' => array ( 0 => __DIR__ . '/..' . '/jakub-onderka/php-console-highlighter/src', ), 'JakubOnderka\\PhpConsoleColor' => array ( 0 => __DIR__ . '/..' . '/jakub-onderka/php-console-color/src', ), ), 'D' => array ( 'Drush' => array ( 0 => __DIR__ . '/../..' . '/lib', ), ), 'C' => array ( 'Consolidation' => array ( 0 => __DIR__ . '/../..' . '/lib', ), ), ); public static $classMap = array ( 'Console_Table' => __DIR__ . '/..' . '/pear/console_table/Table.php', 'File_Iterator' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Iterator.php', 'File_Iterator_Facade' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Facade.php', 'File_Iterator_Factory' => __DIR__ . '/..' . '/phpunit/php-file-iterator/src/Factory.php', 'PHPUnit_Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Exception.php', 'PHPUnit_Extensions_GroupTestSuite' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/GroupTestSuite.php', 'PHPUnit_Extensions_PhptTestCase' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/PhptTestCase.php', 'PHPUnit_Extensions_PhptTestSuite' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/PhptTestSuite.php', 'PHPUnit_Extensions_RepeatedTest' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/RepeatedTest.php', 'PHPUnit_Extensions_TestDecorator' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/TestDecorator.php', 'PHPUnit_Extensions_TicketListener' => __DIR__ . '/..' . '/phpunit/phpunit/src/Extensions/TicketListener.php', 'PHPUnit_Framework_Assert' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Assert.php', 'PHPUnit_Framework_AssertionFailedError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/AssertionFailedError.php', 'PHPUnit_Framework_BaseTestListener' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/BaseTestListener.php', 'PHPUnit_Framework_CodeCoverageException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/CodeCoverageException.php', 'PHPUnit_Framework_Constraint' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint.php', 'PHPUnit_Framework_Constraint_And' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/And.php', 'PHPUnit_Framework_Constraint_ArrayHasKey' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ArrayHasKey.php', 'PHPUnit_Framework_Constraint_ArraySubset' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ArraySubset.php', 'PHPUnit_Framework_Constraint_Attribute' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Attribute.php', 'PHPUnit_Framework_Constraint_Callback' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Callback.php', 'PHPUnit_Framework_Constraint_ClassHasAttribute' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ClassHasAttribute.php', 'PHPUnit_Framework_Constraint_ClassHasStaticAttribute' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ClassHasStaticAttribute.php', 'PHPUnit_Framework_Constraint_Composite' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Composite.php', 'PHPUnit_Framework_Constraint_Count' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Count.php', 'PHPUnit_Framework_Constraint_Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Exception.php', 'PHPUnit_Framework_Constraint_ExceptionCode' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ExceptionCode.php', 'PHPUnit_Framework_Constraint_ExceptionMessage' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ExceptionMessage.php', 'PHPUnit_Framework_Constraint_ExceptionMessageRegExp' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ExceptionMessageRegExp.php', 'PHPUnit_Framework_Constraint_FileExists' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/FileExists.php', 'PHPUnit_Framework_Constraint_GreaterThan' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/GreaterThan.php', 'PHPUnit_Framework_Constraint_IsAnything' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsAnything.php', 'PHPUnit_Framework_Constraint_IsEmpty' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsEmpty.php', 'PHPUnit_Framework_Constraint_IsEqual' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsEqual.php', 'PHPUnit_Framework_Constraint_IsFalse' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsFalse.php', 'PHPUnit_Framework_Constraint_IsIdentical' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsIdentical.php', 'PHPUnit_Framework_Constraint_IsInstanceOf' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsInstanceOf.php', 'PHPUnit_Framework_Constraint_IsJson' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsJson.php', 'PHPUnit_Framework_Constraint_IsNull' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsNull.php', 'PHPUnit_Framework_Constraint_IsTrue' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsTrue.php', 'PHPUnit_Framework_Constraint_IsType' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/IsType.php', 'PHPUnit_Framework_Constraint_JsonMatches' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/JsonMatches.php', 'PHPUnit_Framework_Constraint_JsonMatches_ErrorMessageProvider' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/JsonMatches/ErrorMessageProvider.php', 'PHPUnit_Framework_Constraint_LessThan' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/LessThan.php', 'PHPUnit_Framework_Constraint_Not' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Not.php', 'PHPUnit_Framework_Constraint_ObjectHasAttribute' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/ObjectHasAttribute.php', 'PHPUnit_Framework_Constraint_Or' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Or.php', 'PHPUnit_Framework_Constraint_PCREMatch' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/PCREMatch.php', 'PHPUnit_Framework_Constraint_SameSize' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/SameSize.php', 'PHPUnit_Framework_Constraint_StringContains' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/StringContains.php', 'PHPUnit_Framework_Constraint_StringEndsWith' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/StringEndsWith.php', 'PHPUnit_Framework_Constraint_StringMatches' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/StringMatches.php', 'PHPUnit_Framework_Constraint_StringStartsWith' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/StringStartsWith.php', 'PHPUnit_Framework_Constraint_TraversableContains' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/TraversableContains.php', 'PHPUnit_Framework_Constraint_TraversableContainsOnly' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/TraversableContainsOnly.php', 'PHPUnit_Framework_Constraint_Xor' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Constraint/Xor.php', 'PHPUnit_Framework_Error' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Error.php', 'PHPUnit_Framework_Error_Deprecated' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Error/Deprecated.php', 'PHPUnit_Framework_Error_Notice' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Error/Notice.php', 'PHPUnit_Framework_Error_Warning' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Error/Warning.php', 'PHPUnit_Framework_Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Exception.php', 'PHPUnit_Framework_ExceptionWrapper' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/ExceptionWrapper.php', 'PHPUnit_Framework_ExpectationFailedException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/ExpectationFailedException.php', 'PHPUnit_Framework_IncompleteTest' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/IncompleteTest.php', 'PHPUnit_Framework_IncompleteTestCase' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/IncompleteTestCase.php', 'PHPUnit_Framework_IncompleteTestError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/IncompleteTestError.php', 'PHPUnit_Framework_InvalidCoversTargetError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/InvalidCoversTargetError.php', 'PHPUnit_Framework_InvalidCoversTargetException' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/InvalidCoversTargetException.php', 'PHPUnit_Framework_MockObject_BadMethodCallException' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/BadMethodCallException.php', 'PHPUnit_Framework_MockObject_Builder_Identity' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Identity.php', 'PHPUnit_Framework_MockObject_Builder_InvocationMocker' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/InvocationMocker.php', 'PHPUnit_Framework_MockObject_Builder_Match' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Match.php', 'PHPUnit_Framework_MockObject_Builder_MethodNameMatch' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/MethodNameMatch.php', 'PHPUnit_Framework_MockObject_Builder_Namespace' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Namespace.php', 'PHPUnit_Framework_MockObject_Builder_ParametersMatch' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/ParametersMatch.php', 'PHPUnit_Framework_MockObject_Builder_Stub' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Builder/Stub.php', 'PHPUnit_Framework_MockObject_Exception' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/Exception.php', 'PHPUnit_Framework_MockObject_Generator' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Generator.php', 'PHPUnit_Framework_MockObject_Invocation' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation.php', 'PHPUnit_Framework_MockObject_InvocationMocker' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/InvocationMocker.php', 'PHPUnit_Framework_MockObject_Invocation_Object' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Object.php', 'PHPUnit_Framework_MockObject_Invocation_Static' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invocation/Static.php', 'PHPUnit_Framework_MockObject_Invokable' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Invokable.php', 'PHPUnit_Framework_MockObject_Matcher' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher.php', 'PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyInvokedCount.php', 'PHPUnit_Framework_MockObject_Matcher_AnyParameters' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/AnyParameters.php', 'PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/ConsecutiveParameters.php', 'PHPUnit_Framework_MockObject_Matcher_Invocation' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Invocation.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtIndex.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastCount' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtLeastOnce.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedAtMostCount' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedAtMostCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedCount' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedCount.php', 'PHPUnit_Framework_MockObject_Matcher_InvokedRecorder' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/InvokedRecorder.php', 'PHPUnit_Framework_MockObject_Matcher_MethodName' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/MethodName.php', 'PHPUnit_Framework_MockObject_Matcher_Parameters' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/Parameters.php', 'PHPUnit_Framework_MockObject_Matcher_StatelessInvocation' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Matcher/StatelessInvocation.php', 'PHPUnit_Framework_MockObject_MockBuilder' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockBuilder.php', 'PHPUnit_Framework_MockObject_MockObject' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/MockObject.php', 'PHPUnit_Framework_MockObject_RuntimeException' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Exception/RuntimeException.php', 'PHPUnit_Framework_MockObject_Stub' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub.php', 'PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ConsecutiveCalls.php', 'PHPUnit_Framework_MockObject_Stub_Exception' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Exception.php', 'PHPUnit_Framework_MockObject_Stub_MatcherCollection' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/MatcherCollection.php', 'PHPUnit_Framework_MockObject_Stub_Return' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/Return.php', 'PHPUnit_Framework_MockObject_Stub_ReturnArgument' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnArgument.php', 'PHPUnit_Framework_MockObject_Stub_ReturnCallback' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnCallback.php', 'PHPUnit_Framework_MockObject_Stub_ReturnSelf' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnSelf.php', 'PHPUnit_Framework_MockObject_Stub_ReturnValueMap' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Stub/ReturnValueMap.php', 'PHPUnit_Framework_MockObject_Verifiable' => __DIR__ . '/..' . '/phpunit/phpunit-mock-objects/src/Framework/MockObject/Verifiable.php', 'PHPUnit_Framework_OutputError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/OutputError.php', 'PHPUnit_Framework_RiskyTest' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/RiskyTest.php', 'PHPUnit_Framework_RiskyTestError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/RiskyTestError.php', 'PHPUnit_Framework_SelfDescribing' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SelfDescribing.php', 'PHPUnit_Framework_SkippedTest' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SkippedTest.php', 'PHPUnit_Framework_SkippedTestCase' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SkippedTestCase.php', 'PHPUnit_Framework_SkippedTestError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SkippedTestError.php', 'PHPUnit_Framework_SkippedTestSuiteError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SkippedTestSuiteError.php', 'PHPUnit_Framework_SyntheticError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/SyntheticError.php', 'PHPUnit_Framework_Test' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Test.php', 'PHPUnit_Framework_TestCase' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestCase.php', 'PHPUnit_Framework_TestFailure' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestFailure.php', 'PHPUnit_Framework_TestListener' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestListener.php', 'PHPUnit_Framework_TestResult' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestResult.php', 'PHPUnit_Framework_TestSuite' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestSuite.php', 'PHPUnit_Framework_TestSuite_DataProvider' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/TestSuite/DataProvider.php', 'PHPUnit_Framework_UnintentionallyCoveredCodeError' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/UnintentionallyCoveredCodeError.php', 'PHPUnit_Framework_Warning' => __DIR__ . '/..' . '/phpunit/phpunit/src/Framework/Warning.php', 'PHPUnit_Runner_BaseTestRunner' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/BaseTestRunner.php', 'PHPUnit_Runner_Exception' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Exception.php', 'PHPUnit_Runner_Filter_Factory' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Filter/Factory.php', 'PHPUnit_Runner_Filter_GroupFilterIterator' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Filter/Group.php', 'PHPUnit_Runner_Filter_Group_Exclude' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Filter/Group/Exclude.php', 'PHPUnit_Runner_Filter_Group_Include' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Filter/Group/Include.php', 'PHPUnit_Runner_Filter_Test' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Filter/Test.php', 'PHPUnit_Runner_StandardTestSuiteLoader' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/StandardTestSuiteLoader.php', 'PHPUnit_Runner_TestSuiteLoader' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/TestSuiteLoader.php', 'PHPUnit_Runner_Version' => __DIR__ . '/..' . '/phpunit/phpunit/src/Runner/Version.php', 'PHPUnit_TextUI_Command' => __DIR__ . '/..' . '/phpunit/phpunit/src/TextUI/Command.php', 'PHPUnit_TextUI_ResultPrinter' => __DIR__ . '/..' . '/phpunit/phpunit/src/TextUI/ResultPrinter.php', 'PHPUnit_TextUI_TestRunner' => __DIR__ . '/..' . '/phpunit/phpunit/src/TextUI/TestRunner.php', 'PHPUnit_Util_Blacklist' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Blacklist.php', 'PHPUnit_Util_Configuration' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Configuration.php', 'PHPUnit_Util_ErrorHandler' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/ErrorHandler.php', 'PHPUnit_Util_Fileloader' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Fileloader.php', 'PHPUnit_Util_Filesystem' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Filesystem.php', 'PHPUnit_Util_Filter' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Filter.php', 'PHPUnit_Util_Getopt' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Getopt.php', 'PHPUnit_Util_GlobalState' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/GlobalState.php', 'PHPUnit_Util_InvalidArgumentHelper' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/InvalidArgumentHelper.php', 'PHPUnit_Util_Log_JSON' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Log/JSON.php', 'PHPUnit_Util_Log_JUnit' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Log/JUnit.php', 'PHPUnit_Util_Log_TAP' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Log/TAP.php', 'PHPUnit_Util_PHP' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/PHP.php', 'PHPUnit_Util_PHP_Default' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/PHP/Default.php', 'PHPUnit_Util_PHP_Windows' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/PHP/Windows.php', 'PHPUnit_Util_Printer' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Printer.php', 'PHPUnit_Util_Regex' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Regex.php', 'PHPUnit_Util_String' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/String.php', 'PHPUnit_Util_Test' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Test.php', 'PHPUnit_Util_TestDox_NamePrettifier' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestDox/NamePrettifier.php', 'PHPUnit_Util_TestDox_ResultPrinter' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter.php', 'PHPUnit_Util_TestDox_ResultPrinter_HTML' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter/HTML.php', 'PHPUnit_Util_TestDox_ResultPrinter_Text' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestDox/ResultPrinter/Text.php', 'PHPUnit_Util_TestSuiteIterator' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/TestSuiteIterator.php', 'PHPUnit_Util_Type' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/Type.php', 'PHPUnit_Util_XML' => __DIR__ . '/..' . '/phpunit/phpunit/src/Util/XML.php', 'PHP_CodeCoverage' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage.php', 'PHP_CodeCoverage_Driver' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Driver.php', 'PHP_CodeCoverage_Driver_HHVM' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Driver/HHVM.php', 'PHP_CodeCoverage_Driver_PHPDBG' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Driver/PHPDBG.php', 'PHP_CodeCoverage_Driver_Xdebug' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Driver/Xdebug.php', 'PHP_CodeCoverage_Exception' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Exception.php', 'PHP_CodeCoverage_Exception_UnintentionallyCoveredCode' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Exception/UnintentionallyCoveredCode.php', 'PHP_CodeCoverage_Filter' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Filter.php', 'PHP_CodeCoverage_Report_Clover' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Clover.php', 'PHP_CodeCoverage_Report_Crap4j' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Crap4j.php', 'PHP_CodeCoverage_Report_Factory' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Factory.php', 'PHP_CodeCoverage_Report_HTML' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML.php', 'PHP_CodeCoverage_Report_HTML_Renderer' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer.php', 'PHP_CodeCoverage_Report_HTML_Renderer_Dashboard' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/Dashboard.php', 'PHP_CodeCoverage_Report_HTML_Renderer_Directory' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/Directory.php', 'PHP_CodeCoverage_Report_HTML_Renderer_File' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/File.php', 'PHP_CodeCoverage_Report_Node' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Node.php', 'PHP_CodeCoverage_Report_Node_Directory' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Node/Directory.php', 'PHP_CodeCoverage_Report_Node_File' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Node/File.php', 'PHP_CodeCoverage_Report_Node_Iterator' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Node/Iterator.php', 'PHP_CodeCoverage_Report_PHP' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/PHP.php', 'PHP_CodeCoverage_Report_Text' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/Text.php', 'PHP_CodeCoverage_Report_XML' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML.php', 'PHP_CodeCoverage_Report_XML_Directory' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Directory.php', 'PHP_CodeCoverage_Report_XML_File' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File.php', 'PHP_CodeCoverage_Report_XML_File_Coverage' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Coverage.php', 'PHP_CodeCoverage_Report_XML_File_Method' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Method.php', 'PHP_CodeCoverage_Report_XML_File_Report' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Report.php', 'PHP_CodeCoverage_Report_XML_File_Unit' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/File/Unit.php', 'PHP_CodeCoverage_Report_XML_Node' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Node.php', 'PHP_CodeCoverage_Report_XML_Project' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Project.php', 'PHP_CodeCoverage_Report_XML_Tests' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Tests.php', 'PHP_CodeCoverage_Report_XML_Totals' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Report/XML/Totals.php', 'PHP_CodeCoverage_Util' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Util.php', 'PHP_CodeCoverage_Util_InvalidArgumentHelper' => __DIR__ . '/..' . '/phpunit/php-code-coverage/src/CodeCoverage/Util/InvalidArgumentHelper.php', 'PHP_Timer' => __DIR__ . '/..' . '/phpunit/php-timer/src/Timer.php', 'PHP_Token' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_TokenWithScope' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_TokenWithScopeAndVisibility' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ABSTRACT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AMPERSAND' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AND_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ARRAY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ARRAY_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ASYNC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_AWAIT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BACKTICK' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BAD_CHARACTER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOLEAN_AND' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOLEAN_OR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BOOL_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_BREAK' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CALLABLE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CARET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CASE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CATCH' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CHARACTER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS_C' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLASS_NAME_CONSTANT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLONE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_BRACKET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_CURLY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_SQUARE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CLOSE_TAG' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COALESCE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COLON' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMMA' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMMENT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_COMPILER_HALT_OFFSET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONCAT_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONSTANT_ENCAPSED_STRING' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CONTINUE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_CURLY_OPEN' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DEC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DECLARE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DEFAULT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIV' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DIV_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DNUMBER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DO' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOC_COMMENT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOLLAR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOLLAR_OPEN_CURLY_BRACES' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_ARROW' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_COLON' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_DOUBLE_QUOTES' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ECHO' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELLIPSIS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELSE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ELSEIF' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EMPTY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENCAPSED_AND_WHITESPACE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDDECLARE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDFOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDFOREACH' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDIF' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDSWITCH' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENDWHILE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_END_HEREDOC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ENUM' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EQUALS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EVAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXCLAMATION_MARK' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXIT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_EXTENDS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FILE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FINAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FINALLY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FOREACH' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FUNCTION' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_FUNC_C' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GLOBAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GOTO' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_GT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_HALT_COMPILER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IF' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IMPLEMENTS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IN' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INCLUDE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INCLUDE_ONCE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INLINE_HTML' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INSTANCEOF' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INSTEADOF' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INTERFACE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_INT_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ISSET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_GREATER_OR_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_IDENTICAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_NOT_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_NOT_IDENTICAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_IS_SMALLER_OR_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_Includes' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_JOIN' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_ARROW' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_CP' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LAMBDA_OP' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LINE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LIST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LNUMBER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_AND' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_OR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LOGICAL_XOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_LT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_METHOD_C' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MINUS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MINUS_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MOD_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MULT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_MUL_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NAMESPACE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NEW' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NS_C' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NS_SEPARATOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_NUM_STRING' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OBJECT_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OBJECT_OPERATOR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_ONUMBER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_BRACKET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_CURLY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_SQUARE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_TAG' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OPEN_TAG_WITH_ECHO' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_OR_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PAAMAYIM_NEKUDOTAYIM' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PERCENT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PIPE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PLUS' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PLUS_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_POW' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_POW_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PRINT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PRIVATE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PROTECTED' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_PUBLIC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_QUESTION_MARK' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_REQUIRE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_REQUIRE_ONCE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_RETURN' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SEMICOLON' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SHAPE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SL_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SPACESHIP' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SR_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_START_HEREDOC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STATIC' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_STRING_VARNAME' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SUPER' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_SWITCH' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_Stream' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token/Stream.php', 'PHP_Token_Stream_CachingFactory' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token/Stream/CachingFactory.php', 'PHP_Token_THROW' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TILDE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRAIT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRAIT_C' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TRY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPELIST_GT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_TYPELIST_LT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_UNSET' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_UNSET_CAST' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_USE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_USE_FUNCTION' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_VAR' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_VARIABLE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHERE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHILE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_WHITESPACE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_ATTRIBUTE' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CATEGORY' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CATEGORY_LABEL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_CHILDREN' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_LABEL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_REQUIRED' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TAG_GT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TAG_LT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XHP_TEXT' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_XOR_EQUAL' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_YIELD' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'PHP_Token_YIELD_FROM' => __DIR__ . '/..' . '/phpunit/php-token-stream/src/Token.php', 'SebastianBergmann\\Comparator\\ArrayComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/ArrayComparator.php', 'SebastianBergmann\\Comparator\\Comparator' => __DIR__ . '/..' . '/sebastian/comparator/src/Comparator.php', 'SebastianBergmann\\Comparator\\ComparisonFailure' => __DIR__ . '/..' . '/sebastian/comparator/src/ComparisonFailure.php', 'SebastianBergmann\\Comparator\\DOMNodeComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/DOMNodeComparator.php', 'SebastianBergmann\\Comparator\\DateTimeComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/DateTimeComparator.php', 'SebastianBergmann\\Comparator\\DoubleComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/DoubleComparator.php', 'SebastianBergmann\\Comparator\\ExceptionComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/ExceptionComparator.php', 'SebastianBergmann\\Comparator\\Factory' => __DIR__ . '/..' . '/sebastian/comparator/src/Factory.php', 'SebastianBergmann\\Comparator\\MockObjectComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/MockObjectComparator.php', 'SebastianBergmann\\Comparator\\NumericComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/NumericComparator.php', 'SebastianBergmann\\Comparator\\ObjectComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/ObjectComparator.php', 'SebastianBergmann\\Comparator\\ResourceComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/ResourceComparator.php', 'SebastianBergmann\\Comparator\\ScalarComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/ScalarComparator.php', 'SebastianBergmann\\Comparator\\SplObjectStorageComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/SplObjectStorageComparator.php', 'SebastianBergmann\\Comparator\\TypeComparator' => __DIR__ . '/..' . '/sebastian/comparator/src/TypeComparator.php', 'SebastianBergmann\\Diff\\Chunk' => __DIR__ . '/..' . '/sebastian/diff/src/Chunk.php', 'SebastianBergmann\\Diff\\Diff' => __DIR__ . '/..' . '/sebastian/diff/src/Diff.php', 'SebastianBergmann\\Diff\\Differ' => __DIR__ . '/..' . '/sebastian/diff/src/Differ.php', 'SebastianBergmann\\Diff\\LCS\\LongestCommonSubsequence' => __DIR__ . '/..' . '/sebastian/diff/src/LCS/LongestCommonSubsequence.php', 'SebastianBergmann\\Diff\\LCS\\MemoryEfficientImplementation' => __DIR__ . '/..' . '/sebastian/diff/src/LCS/MemoryEfficientLongestCommonSubsequenceImplementation.php', 'SebastianBergmann\\Diff\\LCS\\TimeEfficientImplementation' => __DIR__ . '/..' . '/sebastian/diff/src/LCS/TimeEfficientLongestCommonSubsequenceImplementation.php', 'SebastianBergmann\\Diff\\Line' => __DIR__ . '/..' . '/sebastian/diff/src/Line.php', 'SebastianBergmann\\Diff\\Parser' => __DIR__ . '/..' . '/sebastian/diff/src/Parser.php', 'SebastianBergmann\\Environment\\Console' => __DIR__ . '/..' . '/sebastian/environment/src/Console.php', 'SebastianBergmann\\Environment\\Runtime' => __DIR__ . '/..' . '/sebastian/environment/src/Runtime.php', 'SebastianBergmann\\Exporter\\Exporter' => __DIR__ . '/..' . '/sebastian/exporter/src/Exporter.php', 'SebastianBergmann\\GlobalState\\Blacklist' => __DIR__ . '/..' . '/sebastian/global-state/src/Blacklist.php', 'SebastianBergmann\\GlobalState\\CodeExporter' => __DIR__ . '/..' . '/sebastian/global-state/src/CodeExporter.php', 'SebastianBergmann\\GlobalState\\Exception' => __DIR__ . '/..' . '/sebastian/global-state/src/Exception.php', 'SebastianBergmann\\GlobalState\\Restorer' => __DIR__ . '/..' . '/sebastian/global-state/src/Restorer.php', 'SebastianBergmann\\GlobalState\\RuntimeException' => __DIR__ . '/..' . '/sebastian/global-state/src/RuntimeException.php', 'SebastianBergmann\\GlobalState\\Snapshot' => __DIR__ . '/..' . '/sebastian/global-state/src/Snapshot.php', 'SebastianBergmann\\RecursionContext\\Context' => __DIR__ . '/..' . '/sebastian/recursion-context/src/Context.php', 'SebastianBergmann\\RecursionContext\\Exception' => __DIR__ . '/..' . '/sebastian/recursion-context/src/Exception.php', 'SebastianBergmann\\RecursionContext\\InvalidArgumentException' => __DIR__ . '/..' . '/sebastian/recursion-context/src/InvalidArgumentException.php', 'SebastianBergmann\\Version' => __DIR__ . '/..' . '/sebastian/version/src/Version.php', 'Text_Template' => __DIR__ . '/..' . '/phpunit/php-text-template/src/Template.php', ); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit13c106add9df9044427bf251f433f228::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit13c106add9df9044427bf251f433f228::$prefixDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInit13c106add9df9044427bf251f433f228::$prefixesPsr0; $loader->classMap = ComposerStaticInit13c106add9df9044427bf251f433f228::$classMap; }, null, ClassLoader::class); } } * 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, PSR-4 and classmap class loader. * * $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 * @see http://www.php-fig.org/psr/psr-0/ * @see http://www.php-fig.org/psr/psr-4/ */ class ClassLoader { // PSR-4 private $prefixLengthsPsr4 = array(); private $prefixDirsPsr4 = array(); private $fallbackDirsPsr4 = array(); // PSR-0 private $prefixesPsr0 = array(); private $fallbackDirsPsr0 = array(); private $useIncludePath = false; private $classMap = array(); private $classMapAuthoritative = false; private $missingClasses = array(); private $apcuPrefix; public function getPrefixes() { if (!empty($this->prefixesPsr0)) { return call_user_func_array('array_merge', $this->prefixesPsr0); } return array(); } public function getPrefixesPsr4() { return $this->prefixDirsPsr4; } public function getFallbackDirs() { return $this->fallbackDirsPsr0; } public function getFallbackDirsPsr4() { return $this->fallbackDirsPsr4; } 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 PSR-0 directories for a given prefix, either * appending or prepending to the ones previously set for this prefix. * * @param string $prefix The prefix * @param array|string $paths The PSR-0 root directories * @param bool $prepend Whether to prepend the directories */ public function add($prefix, $paths, $prepend = false) { if (!$prefix) { if ($prepend) { $this->fallbackDirsPsr0 = array_merge( (array) $paths, $this->fallbackDirsPsr0 ); } else { $this->fallbackDirsPsr0 = array_merge( $this->fallbackDirsPsr0, (array) $paths ); } return; } $first = $prefix[0]; if (!isset($this->prefixesPsr0[$first][$prefix])) { $this->prefixesPsr0[$first][$prefix] = (array) $paths; return; } if ($prepend) { $this->prefixesPsr0[$first][$prefix] = array_merge( (array) $paths, $this->prefixesPsr0[$first][$prefix] ); } else { $this->prefixesPsr0[$first][$prefix] = array_merge( $this->prefixesPsr0[$first][$prefix], (array) $paths ); } } /** * Registers a set of PSR-4 directories for a given namespace, either * appending or prepending to the ones previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param array|string $paths The PSR-4 base directories * @param bool $prepend Whether to prepend the directories * * @throws \InvalidArgumentException */ public function addPsr4($prefix, $paths, $prepend = false) { if (!$prefix) { // Register directories for the root namespace. if ($prepend) { $this->fallbackDirsPsr4 = array_merge( (array) $paths, $this->fallbackDirsPsr4 ); } else { $this->fallbackDirsPsr4 = array_merge( $this->fallbackDirsPsr4, (array) $paths ); } } elseif (!isset($this->prefixDirsPsr4[$prefix])) { // Register directories for a new namespace. $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } elseif ($prepend) { // Prepend directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( (array) $paths, $this->prefixDirsPsr4[$prefix] ); } else { // Append directories for an already registered namespace. $this->prefixDirsPsr4[$prefix] = array_merge( $this->prefixDirsPsr4[$prefix], (array) $paths ); } } /** * Registers a set of PSR-0 directories for a given prefix, * replacing any others previously set for this prefix. * * @param string $prefix The prefix * @param array|string $paths The PSR-0 base directories */ public function set($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr0 = (array) $paths; } else { $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; } } /** * Registers a set of PSR-4 directories for a given namespace, * replacing any others previously set for this namespace. * * @param string $prefix The prefix/namespace, with trailing '\\' * @param array|string $paths The PSR-4 base directories * * @throws \InvalidArgumentException */ public function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$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; } /** * Turns off searching the prefix and fallback directories for classes * that have not been registered with the class map. * * @param bool $classMapAuthoritative */ public function setClassMapAuthoritative($classMapAuthoritative) { $this->classMapAuthoritative = $classMapAuthoritative; } /** * Should class lookup fail if not found in the current class map? * * @return bool */ public function isClassMapAuthoritative() { return $this->classMapAuthoritative; } /** * APCu prefix to use to cache found/not-found classes, if the extension is enabled. * * @param string|null $apcuPrefix */ public function setApcuPrefix($apcuPrefix) { $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; } /** * The APCu prefix in use, or null if APCu caching is not enabled. * * @return string|null */ public function getApcuPrefix() { return $this->apcuPrefix; } /** * 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, null otherwise */ public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } } /** * Finds the path to the file where the class is defined. * * @param string $class The name of the class * * @return string|false The path if found, false otherwise */ public function findFile($class) { // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { return false; } if (null !== $this->apcuPrefix) { $file = apcu_fetch($this->apcuPrefix.$class, $hit); if ($hit) { return $file; } } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if (false === $file && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if (null !== $this->apcuPrefix) { apcu_add($this->apcuPrefix.$class, $file); } if (false === $file) { // Remember that this class does not exist. $this->missingClasses[$class] = true; } return $file; } private function findFileWithExtension($class, $ext) { // PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { $subPath = $class; while (false !== $lastPos = strrpos($subPath, '\\')) { $subPath = substr($subPath, 0, $lastPos); $search = $subPath.'\\'; if (isset($this->prefixDirsPsr4[$search])) { foreach ($this->prefixDirsPsr4[$search] as $dir) { $length = $this->prefixLengthsPsr4[$first][$search]; if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; } return false; } } /** * Scope isolated include. * * Prevents access to $this/self from included files. */ function includeFile($file) { include $file; } getName(); } } parent::__construct($name); if ($commandInfo && $commandInfo->hasAnnotation('command')) { $this->setCommandInfo($commandInfo); $this->setCommandOptions($commandInfo); } } public function setCommandCallback($commandCallback) { $this->commandCallback = $commandCallback; return $this; } public function setCommandProcessor($commandProcessor) { $this->commandProcessor = $commandProcessor; return $this; } public function commandProcessor() { // If someone is using an AnnotatedCommand, and is NOT getting // it from an AnnotatedCommandFactory OR not correctly injecting // a command processor via setCommandProcessor() (ideally via the // DI container), then we'll just give each annotated command its // own command processor. This is not ideal; preferably, there would // only be one instance of the command processor in the application. if (!isset($this->commandProcessor)) { $this->commandProcessor = new CommandProcessor(new HookManager()); } return $this->commandProcessor; } public function getReturnType() { return $this->returnType; } public function setReturnType($returnType) { $this->returnType = $returnType; return $this; } public function getAnnotationData() { return $this->annotationData; } public function setAnnotationData($annotationData) { $this->annotationData = $annotationData; return $this; } public function getTopics() { return $this->topics; } public function setTopics($topics) { $this->topics = $topics; return $this; } public function setCommandInfo($commandInfo) { $this->setDescription($commandInfo->getDescription()); $this->setHelp($commandInfo->getHelp()); $this->setAliases($commandInfo->getAliases()); $this->setAnnotationData($commandInfo->getAnnotations()); $this->setTopics($commandInfo->getTopics()); foreach ($commandInfo->getExampleUsages() as $usage => $description) { $this->addUsageOrExample($usage, $description); } $this->setCommandArguments($commandInfo); $this->setReturnType($commandInfo->getReturnType()); return $this; } protected function addUsageOrExample($usage, $description) { $this->addUsage($usage); if (!empty($description)) { $this->examples[$usage] = $description; } } public function helpAlter(\DomDocument $originalDom) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($commandXML = $dom->createElement('command')); $commandXML->setAttribute('id', $this->getName()); $commandXML->setAttribute('name', $this->getName()); // Get the original element and its top-level elements. $originalCommandXML = $this->getSingleElementByTagName($dom, $originalDom, 'command'); $originalUsagesXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'usages'); $originalDescriptionXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'description'); $originalHelpXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'help'); $originalArgumentsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'arguments'); $originalOptionsXML = $this->getSingleElementByTagName($dom, $originalCommandXML, 'options'); // Keep only the first of the elements $newUsagesXML = $dom->createElement('usages'); $firstUsageXML = $this->getSingleElementByTagName($dom, $originalUsagesXML, 'usage'); $newUsagesXML->appendChild($firstUsageXML); // Create our own elements $newExamplesXML = $dom->createElement('examples'); foreach ($this->examples as $usage => $description) { $newExamplesXML->appendChild($exampleXML = $dom->createElement('example')); $exampleXML->appendChild($usageXML = $dom->createElement('usage', $usage)); $exampleXML->appendChild($descriptionXML = $dom->createElement('description', $description)); } // Create our own elements $newAliasesXML = $dom->createElement('aliases'); foreach ($this->getAliases() as $alias) { $newAliasesXML->appendChild($dom->createElement('alias', $alias)); } // Create our own elements $newTopicsXML = $dom->createElement('topics'); foreach ($this->getTopics() as $topic) { $newTopicsXML->appendChild($topicXML = $dom->createElement('topic', $topic)); } // Place the different elements into the element in the desired order $commandXML->appendChild($newUsagesXML); $commandXML->appendChild($newExamplesXML); $commandXML->appendChild($originalDescriptionXML); $commandXML->appendChild($originalArgumentsXML); $commandXML->appendChild($originalOptionsXML); $commandXML->appendChild($originalHelpXML); $commandXML->appendChild($newAliasesXML); $commandXML->appendChild($newTopicsXML); return $dom; } protected function getSingleElementByTagName($dom, $parent, $tagName) { // There should always be exactly one '' element. $elements = $parent->getElementsByTagName($tagName); $result = $elements->item(0); $result = $dom->importNode($result, true); return $result; } protected function setCommandArguments($commandInfo) { $this->setUsesInputInterface($commandInfo); $this->setUsesOutputInterface($commandInfo); $this->setCommandArgumentsFromParameters($commandInfo); return $this; } /** * Check whether the first parameter is an InputInterface. */ protected function checkUsesInputInterface($params) { $firstParam = reset($params); return $firstParam instanceof InputInterface; } /** * Determine whether this command wants to get its inputs * via an InputInterface or via its command parameters */ protected function setUsesInputInterface($commandInfo) { $params = $commandInfo->getParameters(); $this->usesInputInterface = $this->checkUsesInputInterface($params); return $this; } /** * Determine whether this command wants to send its output directly * to the provided OutputInterface, or whether it will returned * structured output to be processed by the command processor. */ protected function setUsesOutputInterface($commandInfo) { $params = $commandInfo->getParameters(); $index = $this->checkUsesInputInterface($params) ? 1 : 0; $this->usesOutputInterface = (count($params) > $index) && ($params[$index] instanceof OutputInterface); return $this; } protected function setCommandArgumentsFromParameters($commandInfo) { $args = $commandInfo->arguments()->getValues(); foreach ($args as $name => $defaultValue) { $description = $commandInfo->arguments()->getDescription($name); $hasDefault = $commandInfo->arguments()->hasDefault($name); $parameterMode = $this->getCommandArgumentMode($hasDefault, $defaultValue); $this->addArgument($name, $parameterMode, $description, $defaultValue); } return $this; } protected function getCommandArgumentMode($hasDefault, $defaultValue) { if (!$hasDefault) { return InputArgument::REQUIRED; } if (is_array($defaultValue)) { return InputArgument::IS_ARRAY; } return InputArgument::OPTIONAL; } public function setCommandOptions($commandInfo, $automaticOptions = []) { $inputOptions = $commandInfo->inputOptions(); $this->addOptions($inputOptions + $automaticOptions, $automaticOptions); return $this; } public function addOptions($inputOptions, $automaticOptions = []) { foreach ($inputOptions as $name => $inputOption) { $description = $inputOption->getDescription(); if (empty($description) && isset($automaticOptions[$name])) { $description = $automaticOptions[$name]->getDescription(); $inputOption = static::inputOptionSetDescription($inputOption, $description); } $this->getDefinition()->addOption($inputOption); } } protected static function inputOptionSetDescription($inputOption, $description) { // Recover the 'mode' value, because Symfony is stubborn $mode = 0; if ($inputOption->isValueRequired()) { $mode |= InputOption::VALUE_REQUIRED; } if ($inputOption->isValueOptional()) { $mode |= InputOption::VALUE_OPTIONAL; } if ($inputOption->isArray()) { $mode |= InputOption::VALUE_IS_ARRAY; } if (!$mode) { $mode = InputOption::VALUE_NONE; } $inputOption = new InputOption( $inputOption->getName(), $inputOption->getShortcut(), $mode, $description, $inputOption->getDefault() ); return $inputOption; } /** * Returns all of the hook names that may be called for this command. * * @return array */ public function getNames() { return HookManager::getNames($this, $this->commandCallback); } /** * Add any options to this command that are defined by hook implementations */ public function optionsHook() { $this->commandProcessor()->optionsHook( $this, $this->getNames(), $this->annotationData ); } public function optionsHookForHookAnnotations($commandInfoList) { foreach ($commandInfoList as $commandInfo) { $inputOptions = $commandInfo->inputOptions(); $this->addOptions($inputOptions); foreach ($commandInfo->getExampleUsages() as $usage => $description) { if (!in_array($usage, $this->getUsages())) { $this->addUsageOrExample($usage, $description); } } } } /** * {@inheritdoc} */ protected function interact(InputInterface $input, OutputInterface $output) { $this->commandProcessor()->interact( $input, $output, $this->getNames(), $this->annotationData ); } protected function initialize(InputInterface $input, OutputInterface $output) { // Allow the hook manager a chance to provide configuration values, // if there are any registered hooks to do that. $this->commandProcessor()->initializeHook($input, $this->getNames(), $this->annotationData); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { // Validate, run, process, alter, handle results. return $this->commandProcessor()->process( $output, $this->getNames(), $this->commandCallback, $this->createCommandData($input, $output) ); } /** * This function is available for use by a class that may * wish to extend this class rather than use annotations to * define commands. Using this technique does allow for the * use of annotations to define hooks. */ public function processResults(InputInterface $input, OutputInterface $output, $results) { $commandData = $this->createCommandData($input, $output); $commandProcessor = $this->commandProcessor(); $names = $this->getNames(); $results = $commandProcessor->processResults( $names, $results, $commandData ); return $commandProcessor->handleResults( $output, $names, $results, $commandData ); } protected function createCommandData(InputInterface $input, OutputInterface $output) { $commandData = new CommandData( $this->annotationData, $input, $output ); $commandData->setUseIOInterfaces( $this->usesOutputInterface, $this->usesInputInterface ); return $commandData; } } commandProcessor = new CommandProcessor(new HookManager()); $this->addAutomaticOptionProvider($this); } public function setCommandProcessor(CommandProcessor $commandProcessor) { $this->commandProcessor = $commandProcessor; return $this; } /** * @return CommandProcessor */ public function commandProcessor() { return $this->commandProcessor; } /** * Set the 'include all public methods flag'. If true (the default), then * every public method of each commandFile will be used to create commands. * If it is false, then only those public methods annotated with @command * or @name (deprecated) will be used to create commands. */ public function setIncludeAllPublicMethods($includeAllPublicMethods) { $this->includeAllPublicMethods = $includeAllPublicMethods; return $this; } public function getIncludeAllPublicMethods() { return $this->includeAllPublicMethods; } /** * @return HookManager */ public function hookManager() { return $this->commandProcessor()->hookManager(); } /** * Add a listener that is notified immediately before the command * factory creates commands from a commandFile instance. This * listener can use this opportunity to do more setup for the commandFile, * and so on. * * @param CommandCreationListenerInterface $listener */ public function addListener(CommandCreationListenerInterface $listener) { $this->listeners[] = $listener; return $this; } /** * Add a listener that's just a simple 'callable'. * @param callable $listener */ public function addListernerCallback(callable $listener) { $this->addListener(new CommandCreationListener($listener)); return $this; } /** * Call all command creation listeners * * @param object $commandFileInstance */ protected function notify($commandFileInstance) { foreach ($this->listeners as $listener) { $listener->notifyCommandFileAdded($commandFileInstance); } } public function addAutomaticOptionProvider(AutomaticOptionsProviderInterface $optionsProvider) { $this->automaticOptionsProviderList[] = $optionsProvider; } public function addCommandInfoAlterer(CommandInfoAltererInterface $alterer) { $this->commandInfoAlterers[] = $alterer; } /** * n.b. This registers all hooks from the commandfile instance as a side-effect. */ public function createCommandsFromClass($commandFileInstance, $includeAllPublicMethods = null) { // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor. if (!isset($includeAllPublicMethods)) { $includeAllPublicMethods = $this->getIncludeAllPublicMethods(); } $this->notify($commandFileInstance); $commandInfoList = $this->getCommandInfoListFromClass($commandFileInstance); $this->registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance); return $this->createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods); } public function getCommandInfoListFromClass($classNameOrInstance) { $commandInfoList = []; // Ignore special functions, such as __construct and __call, which // can never be commands. $commandMethodNames = array_filter( get_class_methods($classNameOrInstance) ?: [], function ($m) { return !preg_match('#^_#', $m); } ); foreach ($commandMethodNames as $commandMethodName) { $commandInfoList[] = new CommandInfo($classNameOrInstance, $commandMethodName); } return $commandInfoList; } public function createCommandInfo($classNameOrInstance, $commandMethodName) { return new CommandInfo($classNameOrInstance, $commandMethodName); } public function createCommandsFromClassInfo($commandInfoList, $commandFileInstance, $includeAllPublicMethods = null) { // Deprecated: avoid using the $includeAllPublicMethods in favor of the setIncludeAllPublicMethods() accessor. if (!isset($includeAllPublicMethods)) { $includeAllPublicMethods = $this->getIncludeAllPublicMethods(); } return $this->createSelectedCommandsFromClassInfo( $commandInfoList, $commandFileInstance, function ($commandInfo) use ($includeAllPublicMethods) { return static::isCommandMethod($commandInfo, $includeAllPublicMethods); } ); } public function createSelectedCommandsFromClassInfo($commandInfoList, $commandFileInstance, callable $commandSelector) { $commandList = []; foreach ($commandInfoList as $commandInfo) { if ($commandSelector($commandInfo)) { $command = $this->createCommand($commandInfo, $commandFileInstance); $commandList[] = $command; } } return $commandList; } public static function isCommandMethod($commandInfo, $includeAllPublicMethods) { // Ignore everything labeled @hook if ($commandInfo->hasAnnotation('hook')) { return false; } // Include everything labeled @command if ($commandInfo->hasAnnotation('command')) { return true; } // Skip anything named like an accessor ('get' or 'set') if (preg_match('#^(get[A-Z]|set[A-Z])#', $commandInfo->getMethodName())) { return false; } // Default to the setting of 'include all public methods'. return $includeAllPublicMethods; } public function registerCommandHooksFromClassInfo($commandInfoList, $commandFileInstance) { foreach ($commandInfoList as $commandInfo) { if ($commandInfo->hasAnnotation('hook')) { $this->registerCommandHook($commandInfo, $commandFileInstance); } } } /** * Register a command hook given the CommandInfo for a method. * * The hook format is: * * @hook type name type * * For example, the pre-validate hook for the core:init command is: * * @hook pre-validate core:init * * If no command name is provided, then this hook will affect every * command that is defined in the same file. * * If no hook is provided, then we will presume that ALTER_RESULT * is intended. * * @param CommandInfo $commandInfo Information about the command hook method. * @param object $commandFileInstance An instance of the CommandFile class. */ public function registerCommandHook(CommandInfo $commandInfo, $commandFileInstance) { // Ignore if the command info has no @hook if (!$commandInfo->hasAnnotation('hook')) { return; } $hookData = $commandInfo->getAnnotation('hook'); $hook = $this->getNthWord($hookData, 0, HookManager::ALTER_RESULT); $commandName = $this->getNthWord($hookData, 1); // Register the hook $callback = [$commandFileInstance, $commandInfo->getMethodName()]; $this->commandProcessor()->hookManager()->add($callback, $hook, $commandName); // If the hook has options, then also register the commandInfo // with the hook manager, so that we can add options and such to // the commands they hook. if (!$commandInfo->options()->isEmpty()) { $this->commandProcessor()->hookManager()->recordHookOptions($commandInfo, $commandName); } } protected function getNthWord($string, $n, $default = '', $delimiter = ' ') { $words = explode($delimiter, $string); if (!empty($words[$n])) { return $words[$n]; } return $default; } public function createCommand(CommandInfo $commandInfo, $commandFileInstance) { $this->alterCommandInfo($commandInfo, $commandFileInstance); $command = new AnnotatedCommand($commandInfo->getName()); $commandCallback = [$commandFileInstance, $commandInfo->getMethodName()]; $command->setCommandCallback($commandCallback); $command->setCommandProcessor($this->commandProcessor); $command->setCommandInfo($commandInfo); $automaticOptions = $this->callAutomaticOptionsProviders($commandInfo); $command->setCommandOptions($commandInfo, $automaticOptions); // Annotation commands are never bootstrap-aware, but for completeness // we will notify on every created command, as some clients may wish to // use this notification for some other purpose. $this->notify($command); return $command; } /** * Give plugins an opportunity to update the commandInfo */ public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance) { foreach ($this->commandInfoAlterers as $alterer) { $alterer->alterCommandInfo($commandInfo, $commandFileInstance); } } /** * Get the options that are implied by annotations, e.g. @fields implies * that there should be a --fields and a --format option. * * @return InputOption[] */ public function callAutomaticOptionsProviders(CommandInfo $commandInfo) { $automaticOptions = []; foreach ($this->automaticOptionsProviderList as $automaticOptionsProvider) { $automaticOptions += $automaticOptionsProvider->automaticOptions($commandInfo); } return $automaticOptions; } /** * Get the options that are implied by annotations, e.g. @fields implies * that there should be a --fields and a --format option. * * @return InputOption[] */ public function automaticOptions(CommandInfo $commandInfo) { $automaticOptions = []; $formatManager = $this->commandProcessor()->formatterManager(); if ($formatManager) { $annotationData = $commandInfo->getAnnotations()->getArrayCopy(); $formatterOptions = new FormatterOptions($annotationData); $dataType = $commandInfo->getReturnType(); $automaticOptions = $formatManager->automaticOptions($formatterOptions, $dataType); } return $automaticOptions; } } has($key) ? $this[$key] : $default; } public function has($key) { return isset($this[$key]); } public function keys() { return array_keys($this->getArrayCopy()); } } listener = $listener; } public function notifyCommandFileAdded($command) { call_user_func($this->listener, $command); } } annotationData = $annotationData; $this->input = $input; $this->output = $output; $this->usesInputInterface = false; $this->usesOutputInterface = false; $this->includeOptionsInArgs = true; } /** * For internal use only; indicates that the function to be called * should be passed an InputInterface &/or an OutputInterface. * @param booean $usesInputInterface * @param boolean $usesOutputInterface * @return self */ public function setUseIOInterfaces($usesInputInterface, $usesOutputInterface) { $this->usesInputInterface = $usesInputInterface; $this->usesOutputInterface = $usesOutputInterface; return $this; } /** * For backwards-compatibility mode only: disable addition of * options on the end of the arguments list. */ public function setIncludeOptionsInArgs($includeOptionsInArgs) { $this->includeOptionsInArgs = $includeOptionsInArgs; return $this; } public function annotationData() { return $this->annotationData; } public function input() { return $this->input; } public function output() { return $this->output; } public function arguments() { return $this->input->getArguments(); } public function options() { return $this->input->getOptions(); } public function getArgsWithoutAppName() { $args = $this->arguments(); // When called via the Application, the first argument // will be the command name. The Application alters the // input definition to match, adding a 'command' argument // to the beginning. array_shift($args); if ($this->usesInputInterface) { array_unshift($args, $this->input()); } if ($this->usesOutputInterface) { array_unshift($args, $this->output()); } return $args; } public function getArgsAndOptions() { // Get passthrough args, and add the options on the end. $args = $this->getArgsWithoutAppName(); if ($this->includeOptionsInArgs) { $args['options'] = $this->options(); } return $args; } } message = $message; // Ensure the exit code is non-zero. The exit code may have // come from an exception, and those often default to zero if // a specific value is not provided. $this->exitCode = $exitCode == 0 ? 1 : $exitCode; } public function getExitCode() { return $this->exitCode; } public function getOutputData() { return $this->message; } } discoverNamespaced($moduleList, '\Drupal'); * * To discover global commands: * * $commandFiles = $discovery->discover($drupalRoot, '\Drupal'); */ class CommandFileDiscovery { /** @var string[] */ protected $excludeList; /** @var string[] */ protected $searchLocations; /** @var string */ protected $searchPattern = '*Commands.php'; /** @var boolean */ protected $includeFilesAtBase = true; /** @var integer */ protected $searchDepth = 2; public function __construct() { $this->excludeList = ['Exclude']; $this->searchLocations = [ 'Command', 'CliTools', // TODO: Maybe remove ]; } /** * Specify whether to search for files at the base directory * ($directoryList parameter to discover and discoverNamespaced * methods), or only in the directories listed in the search paths. * * @param boolean $includeFilesAtBase */ public function setIncludeFilesAtBase($includeFilesAtBase) { $this->includeFilesAtBase = $includeFilesAtBase; return $this; } /** * Set the list of excludes to add to the finder, replacing * whatever was there before. * * @param array $excludeList The list of directory names to skip when * searching for command files. */ public function setExcludeList($excludeList) { $this->excludeList = $excludeList; return $this; } /** * Add one more location to the exclude list. * * @param string $exclude One directory name to skip when searching * for command files. */ public function addExclude($exclude) { $this->excludeList[] = $exclude; return $this; } /** * Set the search depth. By default, fills immediately in the * base directory are searched, plus all of the search locations * to this specified depth. If the search locations is set to * an empty array, then the base directory is searched to this * depth. */ public function setSearchDepth($searchDepth) { $this->searchDepth = $searchDepth; return $this; } /** * Set the list of search locations to examine in each directory where * command files may be found. This replaces whatever was there before. * * @param array $searchLocations The list of locations to search for command files. */ public function setSearchLocations($searchLocations) { $this->searchLocations = $searchLocations; return $this; } /** * Add one more location to the search location list. * * @param string $location One more relative path to search * for command files. */ public function addSearchLocation($location) { $this->searchLocations[] = $location; return $this; } /** * Specify the pattern / regex used by the finder to search for * command files. */ public function setSearchPattern($searchPattern) { $this->searchPattern = $searchPattern; return $this; } /** * Given a list of directories, e.g. Drupal modules like: * * core/modules/block * core/modules/dblog * modules/default_content * * Discover command files in any of these locations. * * @param string|string[] $directoryList Places to search for commands. * * @return array */ public function discoverNamespaced($directoryList, $baseNamespace = '') { return $this->discover($this->convertToNamespacedList((array)$directoryList), $baseNamespace); } /** * Given a simple list containing paths to directories, where * the last component of the path should appear in the namespace, * after the base namespace, this function will return an * associative array mapping the path's basename (e.g. the module * name) to the directory path. * * Module names must be unique. * * @param string[] $directoryList A list of module locations * * @return array */ public function convertToNamespacedList($directoryList) { $namespacedArray = []; foreach ((array)$directoryList as $directory) { $namespacedArray[basename($directory)] = $directory; } return $namespacedArray; } /** * Search for command files in the specified locations. This is the function that * should be used for all locations that are NOT modules of a framework. * * @param string|string[] $directoryList Places to search for commands. * @return array */ public function discover($directoryList, $baseNamespace = '') { $commandFiles = []; foreach ((array)$directoryList as $key => $directory) { $itemsNamespace = $this->joinNamespace([$baseNamespace, $key]); $commandFiles = array_merge( $commandFiles, $this->discoverCommandFiles($directory, $itemsNamespace), $this->discoverCommandFiles("$directory/src", $itemsNamespace) ); } return $commandFiles; } /** * Search for command files in specific locations within a single directory. * * In each location, we will accept only a few places where command files * can be found. This will reduce the need to search through many unrelated * files. * * The default search locations include: * * . * CliTools * src/CliTools * * The pattern we will look for is any file whose name ends in 'Commands.php'. * A list of paths to found files will be returned. */ protected function discoverCommandFiles($directory, $baseNamespace) { $commandFiles = []; // In the search location itself, we will search for command files // immediately inside the directory only. if ($this->includeFilesAtBase) { $commandFiles = $this->discoverCommandFilesInLocation( $directory, $this->getBaseDirectorySearchDepth(), $baseNamespace ); } // In the other search locations, foreach ($this->searchLocations as $location) { $itemsNamespace = $this->joinNamespace([$baseNamespace, $location]); $commandFiles = array_merge( $commandFiles, $this->discoverCommandFilesInLocation( "$directory/$location", $this->getSearchDepth(), $itemsNamespace ) ); } return $commandFiles; } /** * Return a Finder search depth appropriate for our selected search depth. * * @return string */ protected function getSearchDepth() { return $this->searchDepth <= 0 ? '== 0' : '<= ' . $this->searchDepth; } /** * Return a Finder search depth for the base directory. If the * searchLocations array has been populated, then we will only search * for files immediately inside the base directory; no traversal into * deeper directories will be done, as that would conflict with the * specification provided by the search locations. If there is no * search location, then we will search to whatever depth was specified * by the client. * * @return string */ protected function getBaseDirectorySearchDepth() { if (!empty($this->searchLocations)) { return '== 0'; } return $this->getSearchDepth(); } /** * Search for command files in just one particular location. Returns * an associative array mapping from the pathname of the file to the * classname that it contains. The pathname may be ignored if the search * location is included in the autoloader. * * @param string $directory The location to search * @param string $depth How deep to search (e.g. '== 0' or '< 2') * @param string $baseNamespace Namespace to prepend to each classname * * @return array */ protected function discoverCommandFilesInLocation($directory, $depth, $baseNamespace) { if (!is_dir($directory)) { return []; } $finder = $this->createFinder($directory, $depth); $commands = []; foreach ($finder as $file) { $relativePathName = $file->getRelativePathname(); $relativeNamespaceAndClassname = str_replace( ['/', '.php'], ['\\', ''], $relativePathName ); $classname = $this->joinNamespace([$baseNamespace, $relativeNamespaceAndClassname]); $commandFilePath = $this->joinPaths([$directory, $relativePathName]); $commands[$commandFilePath] = $classname; } return $commands; } /** * Create a Finder object for use in searching a particular directory * location. * * @param string $directory The location to search * @param string $depth The depth limitation * * @return Finder */ protected function createFinder($directory, $depth) { $finder = new Finder(); $finder->files() ->name($this->searchPattern) ->in($directory) ->depth($depth); foreach ($this->excludeList as $item) { $finder->exclude($item); } return $finder; } /** * Combine the items of the provied array into a backslash-separated * namespace string. Empty and numeric items are omitted. * * @param array $namespaceParts List of components of a namespace * * @return string */ protected function joinNamespace(array $namespaceParts) { return $this->joinParts( '\\', $namespaceParts, function ($item) { return !is_numeric($item) && !empty($item); } ); } /** * Combine the items of the provied array into a slash-separated * pathname. Empty items are omitted. * * @param array $pathParts List of components of a path * * @return string */ protected function joinPaths(array $pathParts) { return $this->joinParts( '/', $pathParts, function ($item) { return !empty($item); } ); } /** * Simple wrapper around implode and array_filter. * * @param string $delimiter * @param array $parts * @param callable $filterFunction */ protected function joinParts($delimiter, $parts, $filterFunction) { $parts = array_map( function ($item) use ($delimiter) { return rtrim($item, $delimiter); }, $parts ); return implode( $delimiter, array_filter($parts, $filterFunction) ); } } hookManager = $hookManager; } /** * Return the hook manager * @return HookManager */ public function hookManager() { return $this->hookManager; } public function addPrepareFormatter(PrepareFormatter $preparer) { $this->prepareOptionsList[] = $preparer; } public function setFormatterManager(FormatterManager $formatterManager) { $this->formatterManager = $formatterManager; return $this; } public function setDisplayErrorFunction(callable $fn) { $this->displayErrorFunction = $fn; return $this; } /** * Return the formatter manager * @return FormatterManager */ public function formatterManager() { return $this->formatterManager; } public function initializeHook( InputInterface $input, $names, AnnotationData $annotationData ) { return $this->hookManager()->initializeHook($input, $names, $annotationData); } public function optionsHook( AnnotatedCommand $command, $names, AnnotationData $annotationData ) { $this->hookManager()->optionsHook($command, $names, $annotationData); } public function interact( InputInterface $input, OutputInterface $output, $names, AnnotationData $annotationData ) { return $this->hookManager()->interact($input, $output, $names, $annotationData); } public function process( OutputInterface $output, $names, $commandCallback, CommandData $commandData ) { $result = []; try { $result = $this->validateRunAndAlter( $names, $commandCallback, $commandData ); return $this->handleResults($output, $names, $result, $commandData); } catch (\Exception $e) { $result = new CommandError($e->getMessage(), $e->getCode()); return $this->handleResults($output, $names, $result, $commandData); } } public function validateRunAndAlter( $names, $commandCallback, CommandData $commandData ) { // Validators return any object to signal a validation error; // if the return an array, it replaces the arguments. $validated = $this->hookManager()->validateArguments($names, $commandData); if (is_object($validated)) { return $validated; } // Run the command, alter the results, and then handle output and status $result = $this->runCommandCallback($commandCallback, $commandData); return $this->processResults($names, $result, $commandData); } public function processResults($names, $result, CommandData $commandData) { return $this->hookManager()->alterResult($names, $result, $commandData); } /** * Handle the result output and status code calculation. */ public function handleResults(OutputInterface $output, $names, $result, CommandData $commandData) { $status = $this->hookManager()->determineStatusCode($names, $result); // If the result is an integer and no separate status code was provided, then use the result as the status and do no output. if (is_integer($result) && !isset($status)) { return $result; } $status = $this->interpretStatusCode($status); // Get the structured output, the output stream and the formatter $structuredOutput = $this->hookManager()->extractOutput($names, $result); $output = $this->chooseOutputStream($output, $status); if ($status != 0) { return $this->writeErrorMessage($output, $status, $structuredOutput, $result); } if ($this->dataCanBeFormatted($structuredOutput) && isset($this->formatterManager)) { return $this->writeUsingFormatter($output, $structuredOutput, $commandData); } return $this->writeCommandOutput($output, $structuredOutput); } protected function dataCanBeFormatted($structuredOutput) { if (!isset($this->formatterManager)) { return false; } return is_object($structuredOutput) || is_array($structuredOutput); } /** * Run the main command callback */ protected function runCommandCallback($commandCallback, CommandData $commandData) { $result = false; try { $args = $commandData->getArgsAndOptions(); $result = call_user_func_array($commandCallback, $args); } catch (\Exception $e) { $result = new CommandError($e->getMessage(), $e->getCode()); } return $result; } /** * Determine the formatter that should be used to render * output. * * If the user specified a format via the --format option, * then always return that. Otherwise, return the default * format, unless --pipe was specified, in which case * return the default pipe format, format-pipe. * * n.b. --pipe is a handy option introduced in Drush 2 * (or perhaps even Drush 1) that indicates that the command * should select the output format that is most appropriate * for use in scripts (e.g. to pipe to another command). * * @return string */ protected function getFormat(FormatterOptions $options) { // In Symfony Console, there is no way for us to differentiate // between the user specifying '--format=table', and the user // not specifying --format when the default value is 'table'. // Therefore, we must make --field always override --format; it // cannot become the default value for --format. if ($options->get('field')) { return 'string'; } $defaults = []; if ($options->get('pipe')) { return $options->get('pipe-format', [], 'tsv'); } return $options->getFormat($defaults); } /** * Determine whether we should use stdout or stderr. */ protected function chooseOutputStream(OutputInterface $output, $status) { // If the status code indicates an error, then print the // result to stderr rather than stdout if ($status && ($output instanceof ConsoleOutputInterface)) { return $output->getErrorOutput(); } return $output; } /** * Call the formatter to output the provided data. */ protected function writeUsingFormatter(OutputInterface $output, $structuredOutput, CommandData $commandData) { $formatterOptions = $this->createFormatterOptions($commandData); $format = $this->getFormat($formatterOptions); $this->formatterManager->write( $output, $format, $structuredOutput, $formatterOptions ); return 0; } /** * Create a FormatterOptions object for use in writing the formatted output. * @param CommandData $commandData * @return FormatterOptions */ protected function createFormatterOptions($commandData) { $options = $commandData->input()->getOptions(); $formatterOptions = new FormatterOptions($commandData->annotationData()->getArrayCopy(), $options); foreach ($this->prepareOptionsList as $preparer) { $preparer->prepare($commandData, $formatterOptions); } return $formatterOptions; } /** * Description * @param OutputInterface $output * @param int $status * @param string $structuredOutput * @param mixed $originalResult * @return type */ protected function writeErrorMessage($output, $status, $structuredOutput, $originalResult) { if (isset($this->displayErrorFunction)) { call_user_func($this->displayErrorFunction, $output, $structuredOutput, $status, $originalResult); } else { $this->writeCommandOutput($output, $structuredOutput); } return $status; } /** * If the result object is a string, then print it. */ protected function writeCommandOutput( OutputInterface $output, $structuredOutput ) { // If there is no formatter, we will print strings, // but can do no more than that. if (is_string($structuredOutput)) { $output->writeln($structuredOutput); } return 0; } /** * If a status code was set, then return it; otherwise, * presume success. */ protected function interpretStatusCode($status) { if (isset($status)) { return $status; } return 0; } } hookManager = $hookManager; } /** * {@inheritdoc} */ public function getCustomEventHandlers($eventName) { if (!$this->hookManager) { return []; } return $this->hookManager->getHook($eventName, HookManager::ON_EVENT); } } application = $application; } public function getApplication() { return $this->application; } /** * Run the help command * * @command my-help * @return \Consolidation\AnnotatedCommand\Help\HelpDocument */ public function help($commandName = 'help') { $command = $this->getApplication()->find($commandName); $helpDocument = $this->getHelpDocument($command); return $helpDocument; } /** * Create a help document. */ protected function getHelpDocument($command) { return new HelpDocument($command); } } command = $command; $this->dom = $dom; } /** * Convert data into a \DomDocument. * * @return \DomDocument */ public function getDomData() { return $this->dom; } /** * Create the base help DOM prior to alteration by the Command object. * @param Command $command * @return \DomDocument */ private static function generateBaseHelpDom(Command $command) { // Use Symfony to generate xml text. If other formats are // requested, convert from xml to the desired form. $descriptor = new XmlDescriptor(); return $descriptor->getCommandDocument($command); } /** * Alter the DOM document per the command object * @param Command $command * @param \DomDocument $dom * @return \DomDocument */ private static function alterHelpDocument(Command $command, \DomDocument $dom) { if ($command instanceof HelpDocumentAlter) { $dom = $command->helpAlter($dom); } return $dom; } } hooks; } /** * Add a hook * * @param mixed $callback The callback function to call * @param string $hook The name of the hook to add * @param string $name The name of the command to hook * ('*' for all) */ public function add(callable $callback, $hook, $name = '*') { if (empty($name)) { $name = static::getClassNameFromCallback($callback); } $this->hooks[$name][$hook][] = $callback; return $this; } public function recordHookOptions($commandInfo, $name) { $this->hookOptions[$name][] = $commandInfo; return $this; } public static function getNames($command, $callback) { return array_filter( array_merge( static::getNamesUsingCommands($command), [static::getClassNameFromCallback($callback)] ) ); } protected static function getNamesUsingCommands($command) { return array_merge( [$command->getName()], $command->getAliases() ); } /** * If a command hook does not specify any particular command * name that it should be attached to, then it will be applied * to every command that is defined in the same class as the hook. * This is controlled by using the namespace + class name of * the implementing class of the callback hook. */ protected static function getClassNameFromCallback($callback) { if (!is_array($callback)) { return ''; } $reflectionClass = new \ReflectionClass($callback[0]); return $reflectionClass->getName(); } /** * Add an configuration provider hook * * @param type InitializeHookInterface $provider * @param type $name The name of the command to hook * ('*' for all) */ public function addInitializeHook(InitializeHookInterface $initializeHook, $name = '*') { $this->hooks[$name][self::INITIALIZE][] = $initializeHook; return $this; } /** * Add an option hook * * @param type ValidatorInterface $validator * @param type $name The name of the command to hook * ('*' for all) */ public function addOptionHook(OptionHookInterface $interactor, $name = '*') { $this->hooks[$name][self::INTERACT][] = $interactor; return $this; } /** * Add an interact hook * * @param type ValidatorInterface $validator * @param type $name The name of the command to hook * ('*' for all) */ public function addInteractor(InteractorInterface $interactor, $name = '*') { $this->hooks[$name][self::INTERACT][] = $interactor; return $this; } /** * Add a pre-validator hook * * @param type ValidatorInterface $validator * @param type $name The name of the command to hook * ('*' for all) */ public function addPreValidator(ValidatorInterface $validator, $name = '*') { $this->hooks[$name][self::PRE_ARGUMENT_VALIDATOR][] = $validator; return $this; } /** * Add a validator hook * * @param type ValidatorInterface $validator * @param type $name The name of the command to hook * ('*' for all) */ public function addValidator(ValidatorInterface $validator, $name = '*') { $this->hooks[$name][self::ARGUMENT_VALIDATOR][] = $validator; return $this; } /** * Add a pre-command hook. This is the same as a validator hook, except * that it will run after all of the post-validator hooks. * * @param type ValidatorInterface $preCommand * @param type $name The name of the command to hook * ('*' for all) */ public function addPreCommandHook(ValidatorInterface $preCommand, $name = '*') { $this->hooks[$name][self::PRE_COMMAND_HOOK][] = $preCommand; return $this; } /** * Add a post-command hook. This is the same as a pre-process hook, * except that it will run before the first pre-process hook. * * @param type ProcessResultInterface $postCommand * @param type $name The name of the command to hook * ('*' for all) */ public function addPostCommandHook(ProcessResultInterface $postCommand, $name = '*') { $this->hooks[$name][self::POST_COMMAND_HOOK][] = $postCommand; return $this; } /** * Add a result processor. * * @param type ProcessResultInterface $resultProcessor * @param type $name The name of the command to hook * ('*' for all) */ public function addResultProcessor(ProcessResultInterface $resultProcessor, $name = '*') { $this->hooks[$name][self::PROCESS_RESULT][] = $resultProcessor; return $this; } /** * Add a result alterer. After a result is processed * by a result processor, an alter hook may be used * to convert the result from one form to another. * * @param type AlterResultInterface $resultAlterer * @param type $name The name of the command to hook * ('*' for all) */ public function addAlterResult(AlterResultInterface $resultAlterer, $name = '*') { $this->hooks[$name][self::ALTER_RESULT][] = $resultAlterer; return $this; } /** * Add a status determiner. Usually, a command should return * an integer on error, or a result object on success (which * implies a status code of zero). If a result contains the * status code in some other field, then a status determiner * can be used to call the appropriate accessor method to * determine the status code. This is usually not necessary, * though; a command that fails may return a CommandError * object, which contains a status code and a result message * to display. * @see CommandError::getExitCode() * * @param type StatusDeterminerInterface $statusDeterminer * @param type $name The name of the command to hook * ('*' for all) */ public function addStatusDeterminer(StatusDeterminerInterface $statusDeterminer, $name = '*') { $this->hooks[$name][self::STATUS_DETERMINER][] = $statusDeterminer; return $this; } /** * Add an output extractor. If a command returns an object * object, by default it is passed directly to the output * formatter (if in use) for rendering. If the result object * contains more information than just the data to render, though, * then an output extractor can be used to call the appopriate * accessor method of the result object to get the data to * rendered. This is usually not necessary, though; it is preferable * to have complex result objects implement the OutputDataInterface. * @see OutputDataInterface::getOutputData() * * @param type ExtractOutputInterface $outputExtractor * @param type $name The name of the command to hook * ('*' for all) */ public function addOutputExtractor(ExtractOutputInterface $outputExtractor, $name = '*') { $this->hooks[$name][self::EXTRACT_OUTPUT][] = $outputExtractor; return $this; } public function initializeHook( InputInterface $input, $names, AnnotationData $annotationData ) { $providers = $this->getInitializeHooks($names, $annotationData); foreach ($providers as $provider) { $this->callInjectConfigurationHook($provider, $input, $annotationData); } } public function optionsHook( \Consolidation\AnnotatedCommand\AnnotatedCommand $command, $names, AnnotationData $annotationData ) { $optionHooks = $this->getOptionHooks($names, $annotationData); foreach ($optionHooks as $optionHook) { $this->callOptionHook($optionHook, $command, $annotationData); } $commandInfoList = $this->getHookOptionsForCommand($command); $command->optionsHookForHookAnnotations($commandInfoList); } public function getHookOptionsForCommand($command) { $names = $this->addWildcardHooksToNames($command->getNames(), $command->getAnnotationData()); return $this->getHookOptions($names); } /** * @return CommandInfo[] */ public function getHookOptions($names) { $result = []; foreach ($names as $name) { if (isset($this->hookOptions[$name])) { $result = array_merge($result, $this->hookOptions[$name]); } } return $result; } public function interact( InputInterface $input, OutputInterface $output, $names, AnnotationData $annotationData ) { $interactors = $this->getInteractors($names, $annotationData); foreach ($interactors as $interactor) { $this->callInteractor($interactor, $input, $output, $annotationData); } } public function validateArguments($names, CommandData $commandData) { $validators = $this->getValidators($names, $commandData->annotationData()); foreach ($validators as $validator) { $validated = $this->callValidator($validator, $commandData); if ($validated === false) { return new CommandError(); } if (is_object($validated)) { return $validated; } } } /** * Process result and decide what to do with it. * Allow client to add transformation / interpretation * callbacks. */ public function alterResult($names, $result, CommandData $commandData) { $processors = $this->getProcessResultHooks($names, $commandData->annotationData()); foreach ($processors as $processor) { $result = $this->callProcessor($processor, $result, $commandData); } $alterers = $this->getAlterResultHooks($names, $commandData->annotationData()); foreach ($alterers as $alterer) { $result = $this->callProcessor($alterer, $result, $commandData); } return $result; } /** * Call all status determiners, and see if any of them * know how to convert to a status code. */ public function determineStatusCode($names, $result) { // If the result (post-processing) is an object that // implements ExitCodeInterface, then we will ask it // to give us the status code. if ($result instanceof ExitCodeInterface) { return $result->getExitCode(); } // If the result does not implement ExitCodeInterface, // then we'll see if there is a determiner that can // extract a status code from the result. $determiners = $this->getStatusDeterminers($names); foreach ($determiners as $determiner) { $status = $this->callDeterminer($determiner, $result); if (isset($status)) { return $status; } } } /** * Convert the result object to printable output in * structured form. */ public function extractOutput($names, $result) { if ($result instanceof OutputDataInterface) { return $result->getOutputData(); } $extractors = $this->getOutputExtractors($names); foreach ($extractors as $extractor) { $structuredOutput = $this->callExtractor($extractor, $result); if (isset($structuredOutput)) { return $structuredOutput; } } return $result; } protected function getCommandEventHooks($names) { return $this->getHooks( $names, [ self::PRE_COMMAND_EVENT, self::COMMAND_EVENT, self::POST_COMMAND_EVENT ] ); } protected function getInitializeHooks($names, AnnotationData $annotationData) { return $this->getHooks( $names, [ self::PRE_INITIALIZE, self::INITIALIZE, self::POST_INITIALIZE ], $annotationData ); } protected function getOptionHooks($names, AnnotationData $annotationData) { return $this->getHooks( $names, [ self::PRE_OPTION_HOOK, self::OPTION_HOOK, self::POST_OPTION_HOOK ], $annotationData ); } protected function getInteractors($names, AnnotationData $annotationData) { return $this->getHooks( $names, [ self::PRE_INTERACT, self::INTERACT, self::POST_INTERACT ], $annotationData ); } protected function getValidators($names, AnnotationData $annotationData) { return $this->getHooks( $names, [ self::PRE_ARGUMENT_VALIDATOR, self::ARGUMENT_VALIDATOR, self::POST_ARGUMENT_VALIDATOR, self::PRE_COMMAND_HOOK, self::COMMAND_HOOK, ], $annotationData ); } protected function getProcessResultHooks($names, AnnotationData $annotationData) { return $this->getHooks( $names, [ self::PRE_PROCESS_RESULT, self::PROCESS_RESULT, self::POST_PROCESS_RESULT ], $annotationData ); } protected function getAlterResultHooks($names, AnnotationData $annotationData) { return $this->getHooks( $names, [ self::PRE_ALTER_RESULT, self::ALTER_RESULT, self::POST_ALTER_RESULT, self::POST_COMMAND_HOOK, ], $annotationData ); } protected function getStatusDeterminers($names) { return $this->getHooks( $names, [ self::STATUS_DETERMINER, ] ); } protected function getOutputExtractors($names) { return $this->getHooks( $names, [ self::EXTRACT_OUTPUT, ] ); } /** * Get a set of hooks with the provided name(s). Include the * pre- and post- hooks, and also include the global hooks ('*') * in addition to the named hooks provided. * * @param string|array $names The name of the function being hooked. * @param string[] $hooks A list of hooks (e.g. [HookManager::ALTER_RESULT]) * * @return callable[] */ public function getHooks($names, $hooks, $annotationData = null) { return $this->get($this->addWildcardHooksToNames($names, $annotationData), $hooks); } protected function addWildcardHooksToNames($names, $annotationData = null) { $names = array_merge( (array)$names, ($annotationData == null) ? [] : array_map(function ($item) { return "@$item"; }, $annotationData->keys()) ); $names[] = '*'; return array_unique($names); } /** * Get a set of hooks with the provided name(s). * * @param string|array $names The name of the function being hooked. * @param string[] $hooks The list of hook names (e.g. [HookManager::ALTER_RESULT]) * * @return callable[] */ public function get($names, $hooks) { $result = []; foreach ((array)$hooks as $hook) { foreach ((array)$names as $name) { $result = array_merge($result, $this->getHook($name, $hook)); } } return $result; } /** * Get a single named hook. * * @param string $name The name of the hooked method * @param string $hook The specific hook name (e.g. alter) * * @return callable[] */ public function getHook($name, $hook) { if (isset($this->hooks[$name][$hook])) { return $this->hooks[$name][$hook]; } return []; } protected function callInjectConfigurationHook($provider, $input, AnnotationData $annotationData) { if ($provider instanceof InitializeHookInterface) { return $provider->applyConfiguration($input, $annotationData); } if (is_callable($provider)) { return $provider($input, $annotationData); } } protected function callOptionHook($optionHook, $command, AnnotationData $annotationData) { if ($optionHook instanceof OptionHookInterface) { return $optionHook->getOptions($command, $annotationData); } if (is_callable($optionHook)) { return $optionHook($command, $annotationData); } } protected function callInteractor($interactor, $input, $output, AnnotationData $annotationData) { if ($interactor instanceof InteractorInterface) { return $interactor->interact($input, $output, $annotationData); } if (is_callable($interactor)) { return $interactor($input, $output, $annotationData); } } protected function callValidator($validator, CommandData $commandData) { if ($validator instanceof ValidatorInterface) { return $validator->validate($commandData); } if (is_callable($validator)) { return $validator($commandData); } } protected function callProcessor($processor, $result, CommandData $commandData) { $processed = null; if ($processor instanceof ProcessResultInterface) { $processed = $processor->process($result, $commandData); } if (is_callable($processor)) { $processed = $processor($result, $commandData); } if (isset($processed)) { return $processed; } return $result; } protected function callDeterminer($determiner, $result) { if ($determiner instanceof StatusDeterminerInterface) { return $determiner->determineStatusCode($result); } if (is_callable($determiner)) { return $determiner($result); } } protected function callExtractor($extractor, $result) { if ($extractor instanceof ExtractOutputInterface) { return $extractor->extractOutput($result); } if (is_callable($extractor)) { return $extractor($result); } } /** * @param ConsoleCommandEvent $event */ public function callCommandEventHooks(ConsoleCommandEvent $event) { /* @var Command $command */ $command = $event->getCommand(); $names = [$command->getName()]; $commandEventHooks = $this->getCommandEventHooks($names); foreach ($commandEventHooks as $commandEvent) { if (is_callable($commandEvent)) { $commandEvent($event); } } } public function findAndAddHookOptions($command) { if (!$command instanceof \Consolidation\AnnotatedCommand\AnnotatedCommand) { return; } $command->optionsHook(); } /** * @{@inheritdoc} */ public static function getSubscribedEvents() { return [ConsoleEvents::COMMAND => 'callCommandEventHooks']; } } application = $application; } /** * @param ConsoleCommandEvent $event */ public function alterCommandOptions(ConsoleCommandEvent $event) { /* @var Command $command */ $command = $event->getCommand(); $input = $event->getInput(); if ($command->getName() == 'help') { // Symfony 3.x prepares $input for us; Symfony 2.x, on the other // hand, passes it in prior to binding with the command definition, // so we have to go to a little extra work. It may be inadvisable // to do these steps for commands other than 'help'. if (!$input->hasArgument('command_name')) { $command->ignoreValidationErrors(); $command->mergeApplicationDefinition(); $input->bind($command->getDefinition()); } $nameOfCommandToDescribe = $event->getInput()->getArgument('command_name'); $commandToDescribe = $this->application->find($nameOfCommandToDescribe); $this->findAndAddHookOptions($commandToDescribe); } else { $this->findAndAddHookOptions($command); } } public function findAndAddHookOptions($command) { if (!$command instanceof AnnotatedCommand) { return; } $command->optionsHook(); } /** * @{@inheritdoc} */ public static function getSubscribedEvents() { return [ConsoleEvents::COMMAND => 'alterCommandOptions']; } } defaultWidth = $defaultWidth; } public function setApplication(Application $application) { $this->application = $application; } public function prepare(CommandData $commandData, FormatterOptions $options) { $width = $this->getTerminalWidth(); if (!$width) { $width = $this->defaultWidth; } // Enforce minimum and maximum widths $width = min($width, $this->getMaxWidth($commandData)); $width = max($width, $this->getMinWidth($commandData)); $options->setWidth($width); } protected function getTerminalWidth() { if (!$this->application) { return 0; } $dimensions = $this->application->getTerminalDimensions(); if ($dimensions[0] == null) { return 0; } return $dimensions[0]; } protected function getMaxWidth(CommandData $commandData) { return $this->maxWidth; } protected function getMinWidth(CommandData $commandData) { return $this->minWidth; } } reflection = new \ReflectionMethod($classNameOrInstance, $methodName); $this->methodName = $methodName; $this->otherAnnotations = new AnnotationData(); // Set up a default name for the command from the method name. // This can be overridden via @command or @name annotations. $this->name = $this->convertName($this->reflection->name); $this->options = new DefaultsWithDescriptions($this->determineOptionsFromParameters(), false); $this->arguments = $this->determineAgumentClassifications(); // Remember the name of the last parameter, if it holds the options. // We will use this information to ignore @param annotations for the options. if (!empty($this->options)) { $this->optionParamName = $this->lastParameterName(); } } /** * Recover the method name provided to the constructor. * * @return string */ public function getMethodName() { return $this->methodName; } /** * Return the primary name for this command. * * @return string */ public function getName() { $this->parseDocBlock(); return $this->name; } /** * Set the primary name for this command. * * @param string $name */ public function setName($name) { $this->name = $name; return $this; } public function getReturnType() { $this->parseDocBlock(); return $this->returnType; } public function setReturnType($returnType) { $this->returnType = $returnType; return $this; } /** * Get any annotations included in the docblock comment for the * implementation method of this command that are not already * handled by the primary methods of this class. * * @return AnnotationData */ public function getRawAnnotations() { $this->parseDocBlock(); return $this->otherAnnotations; } /** * Get any annotations included in the docblock comment, * also including default values such as @command. We add * in the default @command annotation late, and only in a * copy of the annotation data because we use the existance * of a @command to indicate that this CommandInfo is * a command, and not a hook or anything else. * * @return AnnotationData */ public function getAnnotations() { return new AnnotationData( $this->getRawAnnotations()->getArrayCopy() + [ 'command' => $this->getName(), ] ); } /** * Return a specific named annotation for this command. * * @param string $annotation The name of the annotation. * @return string */ public function getAnnotation($annotation) { // hasAnnotation parses the docblock if (!$this->hasAnnotation($annotation)) { return null; } return $this->otherAnnotations[$annotation]; } /** * Check to see if the specified annotation exists for this command. * * @param string $annotation The name of the annotation. * @return boolean */ public function hasAnnotation($annotation) { $this->parseDocBlock(); return isset($this->otherAnnotations[$annotation]); } /** * Save any tag that we do not explicitly recognize in the * 'otherAnnotations' map. */ public function addAnnotation($name, $content) { $this->otherAnnotations[$name] = $content; } /** * Remove an annotation that was previoudly set. */ public function removeAnnotation($name) { unset($this->otherAnnotations[$name]); } /** * Get the synopsis of the command (~first line). * * @return string */ public function getDescription() { $this->parseDocBlock(); return $this->description; } /** * Set the command description. * * @param string $description The description to set. */ public function setDescription($description) { $this->description = $description; return $this; } /** * Get the help text of the command (the description) */ public function getHelp() { $this->parseDocBlock(); return $this->help; } /** * Set the help text for this command. * * @param string $help The help text. */ public function setHelp($help) { $this->help = $help; return $this; } /** * Return the list of aliases for this command. * @return string[] */ public function getAliases() { $this->parseDocBlock(); return $this->aliases; } /** * Set aliases that can be used in place of the command's primary name. * * @param string|string[] $aliases */ public function setAliases($aliases) { if (is_string($aliases)) { $aliases = explode(',', static::convertListToCommaSeparated($aliases)); } $this->aliases = array_filter($aliases); return $this; } /** * Return the examples for this command. This is @usage instead of * @example because the later is defined by the phpdoc standard to * be example method calls. * * @return string[] */ public function getExampleUsages() { $this->parseDocBlock(); return $this->exampleUsage; } /** * Add an example usage for this command. * * @param string $usage An example of the command, including the command * name and all of its example arguments and options. * @param string $description An explanation of what the example does. */ public function setExampleUsage($usage, $description) { $this->exampleUsage[$usage] = $description; return $this; } /** * Return the topics for this command. * * @return string[] */ public function getTopics() { if (!$this->hasAnnotation('topics')) { return []; } $topics = $this->getAnnotation('topics'); return explode(',', trim($topics)); } /** * Return the list of refleaction parameters. * * @return ReflectionParameter[] */ public function getParameters() { return $this->reflection->getParameters(); } /** * Descriptions of commandline arguements for this command. * * @return DefaultsWithDescriptions */ public function arguments() { return $this->arguments; } /** * Descriptions of commandline options for this command. * * @return DefaultsWithDescriptions */ public function options() { return $this->options; } /** * Return the name of the last parameter if it holds the options. */ public function optionParamName() { return $this->optionParamName; } /** * Get the inputOptions for the options associated with this CommandInfo * object, e.g. via @option annotations, or from * $options = ['someoption' => 'defaultvalue'] in the command method * parameter list. * * @return InputOption[] */ public function inputOptions() { $explicitOptions = []; $opts = $this->options()->getValues(); foreach ($opts as $name => $defaultValue) { $description = $this->options()->getDescription($name); $fullName = $name; $shortcut = ''; if (strpos($name, '|')) { list($fullName, $shortcut) = explode('|', $name, 2); } if (is_bool($defaultValue)) { $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_NONE, $description); } elseif ($defaultValue === InputOption::VALUE_REQUIRED) { $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_REQUIRED, $description); } else { $explicitOptions[$fullName] = new InputOption($fullName, $shortcut, InputOption::VALUE_OPTIONAL, $description, $defaultValue); } } return $explicitOptions; } /** * An option might have a name such as 'silent|s'. In this * instance, we will allow the @option or @default tag to * reference the option only by name (e.g. 'silent' or 's' * instead of 'silent|s'). * * @param string $optionName * @return string */ public function findMatchingOption($optionName) { // Exit fast if there's an exact match if ($this->options->exists($optionName)) { return $optionName; } $existingOptionName = $this->findExistingOption($optionName); if (isset($existingOptionName)) { return $existingOptionName; } return $this->findOptionAmongAlternatives($optionName); } /** * @param string $optionName * @return string */ protected function findOptionAmongAlternatives($optionName) { // Check the other direction: if the annotation contains @silent|s // and the options array has 'silent|s'. $checkMatching = explode('|', $optionName); if (count($checkMatching) > 1) { foreach ($checkMatching as $checkName) { if ($this->options->exists($checkName)) { $this->options->rename($checkName, $optionName); return $optionName; } } } return $optionName; } /** * @param string $optionName * @return string|null */ protected function findExistingOption($optionName) { // Check to see if we can find the option name in an existing option, // e.g. if the options array has 'silent|s' => false, and the annotation // is @silent. foreach ($this->options()->getValues() as $name => $default) { if (in_array($optionName, explode('|', $name))) { return $name; } } } /** * Examine the parameters of the method for this command, and * build a list of commandline arguements for them. * * @return array */ protected function determineAgumentClassifications() { $result = new DefaultsWithDescriptions(); $params = $this->reflection->getParameters(); $optionsFromParameters = $this->determineOptionsFromParameters(); if (!empty($optionsFromParameters)) { array_pop($params); } foreach ($params as $param) { $this->addParameterToResult($result, $param); } return $result; } /** * Examine the provided parameter, and determine whether it * is a parameter that will be filled in with a positional * commandline argument. */ protected function addParameterToResult($result, $param) { // Commandline arguments must be strings, so ignore any // parameter that is typehinted to any non-primative class. if ($param->getClass() != null) { return; } $result->add($param->name); if ($param->isDefaultValueAvailable()) { $defaultValue = $param->getDefaultValue(); if (!$this->isAssoc($defaultValue)) { $result->setDefaultValue($param->name, $defaultValue); } } elseif ($param->isArray()) { $result->setDefaultValue($param->name, []); } } /** * Examine the parameters of the method for this command, and determine * the disposition of the options from them. * * @return array */ protected function determineOptionsFromParameters() { $params = $this->reflection->getParameters(); if (empty($params)) { return []; } $param = end($params); if (!$param->isDefaultValueAvailable()) { return []; } if (!$this->isAssoc($param->getDefaultValue())) { return []; } return $param->getDefaultValue(); } protected function lastParameterName() { $params = $this->reflection->getParameters(); $param = end($params); if (!$param) { return ''; } return $param->name; } /** * Helper; determine if an array is associative or not. An array * is not associative if its keys are numeric, and numbered sequentially * from zero. All other arrays are considered to be associative. * * @param arrau $arr The array * @return boolean */ protected function isAssoc($arr) { if (!is_array($arr)) { return false; } return array_keys($arr) !== range(0, count($arr) - 1); } /** * Convert from a method name to the corresponding command name. A * method 'fooBar' will become 'foo:bar', and 'fooBarBazBoz' will * become 'foo:bar-baz-boz'. * * @param string $camel method name. * @return string */ protected function convertName($camel) { $splitter="-"; $camel=preg_replace('/(?!^)[[:upper:]][[:lower:]]/', '$0', preg_replace('/(?!^)[[:upper:]]+/', $splitter.'$0', $camel)); $camel = preg_replace("/$splitter/", ':', $camel, 1); return strtolower($camel); } /** * Parse the docBlock comment for this command, and set the * fields of this class with the data thereby obtained. */ protected function parseDocBlock() { if (!$this->docBlockIsParsed) { // The parse function will insert data from the provided method // into this object, using our accessors. CommandDocBlockParserFactory::parse($this, $this->reflection); $this->docBlockIsParsed = true; } } /** * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c', * convert the data into the last of these forms. */ protected static function convertListToCommaSeparated($text) { return preg_replace('#[ \t\n\r,]+#', ',', $text); } } values = $values; $this->hasDefault = []; $this->descriptions = []; $this->defaultDefault = $defaultDefault; } /** * Return just the key : default values mapping * * @return array */ public function getValues() { return $this->values; } /** * Return true if this set of options is empty * * @return */ public function isEmpty() { return empty($this->values); } /** * Check to see whether the speicifed key exists in the collection. * * @param string $key * @return boolean */ public function exists($key) { return array_key_exists($key, $this->values); } /** * Get the value of one entry. * * @param string $key The key of the item. * @return string */ public function get($key) { if (array_key_exists($key, $this->values)) { return $this->values[$key]; } return $this->defaultDefault; } /** * Get the description of one entry. * * @param string $key The key of the item. * @return string */ public function getDescription($key) { if (array_key_exists($key, $this->descriptions)) { return $this->descriptions[$key]; } return ''; } /** * Add another argument to this command. * * @param string $key Name of the argument. * @param string $description Help text for the argument. * @param mixed $defaultValue The default value for the argument. */ public function add($key, $description = '', $defaultValue = null) { if (!$this->exists($key) || isset($defaultValue)) { $this->values[$key] = isset($defaultValue) ? $defaultValue : $this->defaultDefault; } unset($this->descriptions[$key]); if (!empty($description)) { $this->descriptions[$key] = $description; } } /** * Change the default value of an entry. * * @param string $key * @param mixed $defaultValue */ public function setDefaultValue($key, $defaultValue) { $this->values[$key] = $defaultValue; $this->hasDefault[$key] = true; return $this; } /** * Check to see if the named argument definitively has a default value. * * @param string $key * @return bool */ public function hasDefault($key) { return array_key_exists($key, $this->hasDefault); } /** * Remove an entry * * @param string $key The entry to remove */ public function clear($key) { unset($this->values[$key]); unset($this->descriptions[$key]); } /** * Rename an existing option to something else. */ public function rename($oldName, $newName) { $this->add($newName, $this->getDescription($oldName), $this->get($oldName)); $this->clear($oldName); } } 'processCommandTag', 'name' => 'processCommandTag', 'arg' => 'processArgumentTag', 'param' => 'processParamTag', 'return' => 'processReturnTag', 'option' => 'processOptionTag', 'default' => 'processDefaultTag', 'aliases' => 'processAliases', 'usage' => 'processUsageTag', 'description' => 'processAlternateDescriptionTag', 'desc' => 'processAlternateDescriptionTag', ]; public function __construct(CommandInfo $commandInfo, \ReflectionMethod $reflection) { $this->commandInfo = $commandInfo; $this->reflection = $reflection; } protected function processAllTags($phpdoc) { // Iterate over all of the tags, and process them as necessary. foreach ($phpdoc->getTags() as $tag) { $processFn = [$this, 'processGenericTag']; if (array_key_exists($tag->getName(), $this->tagProcessors)) { $processFn = [$this, $this->tagProcessors[$tag->getName()]]; } $processFn($tag); } } abstract protected function getTagContents($tag); /** * Parse the docBlock comment for this command, and set the * fields of this class with the data thereby obtained. */ abstract public function parse(); /** * Save any tag that we do not explicitly recognize in the * 'otherAnnotations' map. */ protected function processGenericTag($tag) { $this->commandInfo->addAnnotation($tag->getName(), $this->getTagContents($tag)); } /** * Set the name of the command from a @command or @name annotation. */ protected function processCommandTag($tag) { $commandName = $this->getTagContents($tag); $this->commandInfo->setName($commandName); // We also store the name in the 'other annotations' so that is is // possible to determine if the method had a @command annotation. $this->commandInfo->addAnnotation($tag->getName(), $commandName); } /** * The @description and @desc annotations may be used in * place of the synopsis (which we call 'description'). * This is discouraged. * * @deprecated */ protected function processAlternateDescriptionTag($tag) { $this->commandInfo->setDescription($this->getTagContents($tag)); } /** * Store the data from a @arg annotation in our argument descriptions. */ protected function processArgumentTag($tag) { if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) { return; } $this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $match); } /** * Store the data from an @option annotation in our option descriptions. */ protected function processOptionTag($tag) { if (!$this->pregMatchOptionNameAndDescription((string)$tag->getDescription(), $match)) { return; } $this->addOptionOrArgumentTag($tag, $this->commandInfo->options(), $match); } protected function addOptionOrArgumentTag($tag, DefaultsWithDescriptions $set, $nameAndDescription) { $variableName = $this->commandInfo->findMatchingOption($nameAndDescription['name']); $desc = $nameAndDescription['description']; $description = static::removeLineBreaks($desc); $set->add($variableName, $description); } /** * Store the data from a @default annotation in our argument or option store, * as appropriate. */ protected function processDefaultTag($tag) { if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) { return; } $variableName = $match['name']; $defaultValue = $this->interpretDefaultValue($match['description']); if ($this->commandInfo->arguments()->exists($variableName)) { $this->commandInfo->arguments()->setDefaultValue($variableName, $defaultValue); return; } $variableName = $this->commandInfo->findMatchingOption($variableName); if ($this->commandInfo->options()->exists($variableName)) { $this->commandInfo->options()->setDefaultValue($variableName, $defaultValue); } } /** * Store the data from a @usage annotation in our example usage list. */ protected function processUsageTag($tag) { $lines = explode("\n", $this->getTagContents($tag)); $usage = array_shift($lines); $description = static::removeLineBreaks(implode("\n", $lines)); $this->commandInfo->setExampleUsage($usage, $description); } /** * Process the comma-separated list of aliases */ protected function processAliases($tag) { $this->commandInfo->setAliases((string)$tag->getDescription()); } /** * Store the data from a @param annotation in our argument descriptions. */ protected function processParamTag($tag) { $variableName = $tag->getVariableName(); $variableName = str_replace('$', '', $variableName); $description = static::removeLineBreaks((string)$tag->getDescription()); if ($variableName == $this->commandInfo->optionParamName()) { return; } $this->commandInfo->arguments()->add($variableName, $description); } /** * Store the data from a @return annotation in our argument descriptions. */ abstract protected function processReturnTag($tag); protected function interpretDefaultValue($defaultValue) { $defaults = [ 'null' => null, 'true' => true, 'false' => false, "''" => '', '[]' => [], ]; foreach ($defaults as $defaultName => $defaultTypedValue) { if ($defaultValue == $defaultName) { return $defaultTypedValue; } } return $defaultValue; } /** * Given a docblock description in the form "$variable description", * return the variable name and description via the 'match' parameter. */ protected function pregMatchNameAndDescription($source, &$match) { $nameRegEx = '\\$(?P[^ \t]+)[ \t]+'; $descriptionRegEx = '(?P.*)'; $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s"; return preg_match($optionRegEx, $source, $match); } /** * Given a docblock description in the form "$variable description", * return the variable name and description via the 'match' parameter. */ protected function pregMatchOptionNameAndDescription($source, &$match) { // Strip type and $ from the text before the @option name, if present. $source = preg_replace('/^[a-zA-Z]* ?\\$/', '', $source); $nameRegEx = '(?P[^ \t]+)[ \t]+'; $descriptionRegEx = '(?P.*)'; $optionRegEx = "/{$nameRegEx}{$descriptionRegEx}/s"; return preg_match($optionRegEx, $source, $match); } /** * Given a list that might be 'a b c' or 'a, b, c' or 'a,b,c', * convert the data into the last of these forms. */ protected static function convertListToCommaSeparated($text) { return preg_replace('#[ \t\n\r,]+#', ',', $text); } /** * Take a multiline description and convert it into a single * long unbroken line. */ protected static function removeLineBreaks($text) { return trim(preg_replace('#[ \t\n\r]+#', ' ', $text)); } } reflection->getDocComment(); $phpdoc = new DocBlock($docblockComment); // First set the description (synopsis) and help. $this->commandInfo->setDescription((string)$phpdoc->getShortDescription()); $this->commandInfo->setHelp((string)$phpdoc->getLongDescription()); $this->processAllTags($phpdoc); } protected function getTagContents($tag) { return $tag->getContent(); } /** * Store the data from a @arg annotation in our argument descriptions. */ protected function processArgumentTag($tag) { if (!$this->pregMatchNameAndDescription((string)$tag->getDescription(), $match)) { return; } $this->addOptionOrArgumentTag($tag, $this->commandInfo->arguments(), $match); } /** * Store the data from a @param annotation in our argument descriptions. */ protected function processParamTag($tag) { if (!$tag instanceof ParamTag) { return; } return parent::processParamTag($tag); } /** * Store the data from a @return annotation in our argument descriptions. */ protected function processReturnTag($tag) { if (!$tag instanceof ReturnTag) { return; } $this->commandInfo->setReturnType($tag->getType()); } } reflection->getDocComment(); if (empty($docComment)) { return; } $phpdoc = $this->createDocBlock(); // First set the description (synopsis) and help. $this->commandInfo->setDescription((string)$phpdoc->getSummary()); $this->commandInfo->setHelp((string)$phpdoc->getDescription()); $this->processAllTags($phpdoc); } public function createDocBlock() { $docBlockFactory = \phpDocumentor\Reflection\DocBlockFactory::createInstance(); $contextFactory = new \phpDocumentor\Reflection\Types\ContextFactory(); return $docBlockFactory->create( $this->reflection, $contextFactory->createFromReflector($this->reflection) ); } protected function getTagContents($tag) { return (string)$tag; } /** * Store the data from a @param annotation in our argument descriptions. */ protected function processParamTag($tag) { if (!$tag instanceof Param) { return; } return parent::processParamTag($tag); } /** * Store the data from a @return annotation in our argument descriptions. */ protected function processReturnTag($tag) { if (!$tag instanceof Return_) { return; } // If there is a spurrious trailing space on the return type, remove it. $this->commandInfo->setReturnType(trim($this->getTagContents($tag))); } } parse(); } private static function create(CommandInfo $commandInfo, \ReflectionMethod $reflection) { if (static::hasReflectionDocBlock3()) { return new CommandDocBlockParser3($commandInfo, $reflection); } return new CommandDocBlockParser2($commandInfo, $reflection); } private static function hasReflectionDocBlock3() { return class_exists('phpDocumentor\Reflection\DocBlockFactory') && class_exists('phpDocumentor\Reflection\Types\ContextFactory'); } } getName() == 'ArrayObject')) { return 'an array'; } return 'an instance of ' . $data->getName(); } if (is_string($data)) { return 'a string'; } if (is_object($data)) { return 'an instance of ' . get_class($data); } throw new \Exception("Undescribable data error: " . var_export($data, true)); } protected static function describeAllowedTypes($allowedTypes) { if (is_array($allowedTypes) && !empty($allowedTypes)) { if (count($allowedTypes) > 1) { return static::describeListOfAllowedTypes($allowedTypes); } $allowedTypes = $allowedTypes[0]; } return static::describeDataType($allowedTypes); } protected static function describeListOfAllowedTypes($allowedTypes) { $descriptions = []; foreach ($allowedTypes as $oneAllowedType) { $descriptions[] = static::describeDataType($oneAllowedType); } if (count($descriptions) == 2) { return "either {$descriptions[0]} or {$descriptions[1]}"; } $lastDescription = array_pop($descriptions); $otherDescriptions = implode(', ', $descriptions); return "one of $otherDescriptions or $lastDescription"; } } '\Consolidation\OutputFormatters\Formatters\StringFormatter', 'yaml' => '\Consolidation\OutputFormatters\Formatters\YamlFormatter', 'xml' => '\Consolidation\OutputFormatters\Formatters\XmlFormatter', 'json' => '\Consolidation\OutputFormatters\Formatters\JsonFormatter', 'print-r' => '\Consolidation\OutputFormatters\Formatters\PrintRFormatter', 'php' => '\Consolidation\OutputFormatters\Formatters\SerializeFormatter', 'var_export' => '\Consolidation\OutputFormatters\Formatters\VarExportFormatter', 'list' => '\Consolidation\OutputFormatters\Formatters\ListFormatter', 'csv' => '\Consolidation\OutputFormatters\Formatters\CsvFormatter', 'tsv' => '\Consolidation\OutputFormatters\Formatters\TsvFormatter', 'table' => '\Consolidation\OutputFormatters\Formatters\TableFormatter', 'sections' => '\Consolidation\OutputFormatters\Formatters\SectionsFormatter', ]; foreach ($defaultFormatters as $id => $formatterClassname) { $formatter = new $formatterClassname; $this->addFormatter($id, $formatter); } $this->addFormatter('', $this->formatters['string']); } public function addDefaultSimplifiers() { // Add our default array simplifier (DOMDocument to array) $this->addSimplifier(new DomToArraySimplifier()); } /** * Add a formatter * * @param string $key the identifier of the formatter to add * @param string $formatter the class name of the formatter to add * @return FormatterManager */ public function addFormatter($key, FormatterInterface $formatter) { $this->formatters[$key] = $formatter; return $this; } /** * Add a simplifier * * @param SimplifyToArrayInterface $simplifier the array simplifier to add * @return FormatterManager */ public function addSimplifier(SimplifyToArrayInterface $simplifier) { $this->arraySimplifiers[] = $simplifier; return $this; } /** * Return a set of InputOption based on the annotations of a command. * @param FormatterOptions $options * @return InputOption[] */ public function automaticOptions(FormatterOptions $options, $dataType) { $automaticOptions = []; // At the moment, we only support automatic options for --format // and --fields, so exit if the command returns no data. if (!isset($dataType)) { return []; } $validFormats = $this->validFormats($dataType); if (empty($validFormats)) { return []; } $availableFields = $options->get(FormatterOptions::FIELD_LABELS); $hasDefaultStringField = $options->get(FormatterOptions::DEFAULT_STRING_FIELD); $defaultFormat = $hasDefaultStringField ? 'string' : ($availableFields ? 'table' : 'yaml'); if (count($validFormats) > 1) { // Make an input option for --format $description = 'Format the result data. Available formats: ' . implode(',', $validFormats); $automaticOptions[FormatterOptions::FORMAT] = new InputOption(FormatterOptions::FORMAT, '', InputOption::VALUE_OPTIONAL, $description, $defaultFormat); } if ($availableFields) { $defaultFields = $options->get(FormatterOptions::DEFAULT_FIELDS, [], ''); $description = 'Available fields: ' . implode(', ', $this->availableFieldsList($availableFields)); $automaticOptions[FormatterOptions::FIELDS] = new InputOption(FormatterOptions::FIELDS, '', InputOption::VALUE_OPTIONAL, $description, $defaultFields); $automaticOptions[FormatterOptions::FIELD] = new InputOption(FormatterOptions::FIELD, '', InputOption::VALUE_OPTIONAL, "Select just one field, and force format to 'string'.", ''); } return $automaticOptions; } /** * Given a list of available fields, return a list of field descriptions. * @return string[] */ protected function availableFieldsList($availableFields) { return array_map( function ($key) use ($availableFields) { return $availableFields[$key] . " ($key)"; }, array_keys($availableFields) ); } /** * Return the identifiers for all valid data types that have been registered. * * @param mixed $dataType \ReflectionObject or other description of the produced data type * @return array */ public function validFormats($dataType) { $validFormats = []; foreach ($this->formatters as $formatId => $formatterName) { $formatter = $this->getFormatter($formatId); if (!empty($formatId) && $this->isValidFormat($formatter, $dataType)) { $validFormats[] = $formatId; } } sort($validFormats); return $validFormats; } public function isValidFormat(FormatterInterface $formatter, $dataType) { if (is_array($dataType)) { $dataType = new \ReflectionClass('\ArrayObject'); } if (!is_object($dataType) && !class_exists($dataType)) { return false; } if (!$dataType instanceof \ReflectionClass) { $dataType = new \ReflectionClass($dataType); } return $this->isValidDataType($formatter, $dataType); } public function isValidDataType(FormatterInterface $formatter, \ReflectionClass $dataType) { if ($this->canSimplifyToArray($dataType)) { if ($this->isValidFormat($formatter, [])) { return true; } } // If the formatter does not implement ValidationInterface, then // it is presumed that the formatter only accepts arrays. if (!$formatter instanceof ValidationInterface) { return $dataType->isSubclassOf('ArrayObject') || ($dataType->getName() == 'ArrayObject'); } return $formatter->isValidDataType($dataType); } /** * Format and write output * * @param OutputInterface $output Output stream to write to * @param string $format Data format to output in * @param mixed $structuredOutput Data to output * @param FormatterOptions $options Formatting options */ public function write(OutputInterface $output, $format, $structuredOutput, FormatterOptions $options) { $formatter = $this->getFormatter((string)$format); if (!is_string($structuredOutput) && !$this->isValidFormat($formatter, $structuredOutput)) { $validFormats = $this->validFormats($structuredOutput); throw new InvalidFormatException((string)$format, $structuredOutput, $validFormats); } // Give the formatter a chance to override the options $options = $this->overrideOptions($formatter, $structuredOutput, $options); $structuredOutput = $this->validateAndRestructure($formatter, $structuredOutput, $options); $formatter->write($output, $structuredOutput, $options); } protected function validateAndRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options) { // Give the formatter a chance to do something with the // raw data before it is restructured. $overrideRestructure = $this->overrideRestructure($formatter, $structuredOutput, $options); if ($overrideRestructure) { return $overrideRestructure; } // Restructure the output data (e.g. select fields to display, etc.). $restructuredOutput = $this->restructureData($structuredOutput, $options); // Make sure that the provided data is in the correct format for the selected formatter. $restructuredOutput = $this->validateData($formatter, $restructuredOutput, $options); // Give the original data a chance to re-render the structured // output after it has been restructured and validated. $restructuredOutput = $this->renderData($formatter, $structuredOutput, $restructuredOutput, $options); return $restructuredOutput; } /** * Fetch the requested formatter. * * @param string $format Identifier for requested formatter * @return FormatterInterface */ public function getFormatter($format) { // The client must inject at least one formatter before asking for // any formatters; if not, we will provide all of the usual defaults // as a convenience. if (empty($this->formatters)) { $this->addDefaultFormatters(); $this->addDefaultSimplifiers(); } if (!$this->hasFormatter($format)) { throw new UnknownFormatException($format); } $formatter = $this->formatters[$format]; return $formatter; } /** * Test to see if the stipulated format exists */ public function hasFormatter($format) { return array_key_exists($format, $this->formatters); } /** * Render the data as necessary (e.g. to select or reorder fields). * * @param FormatterInterface $formatter * @param mixed $originalData * @param mixed $restructuredData * @param FormatterOptions $options Formatting options * @return mixed */ public function renderData(FormatterInterface $formatter, $originalData, $restructuredData, FormatterOptions $options) { if ($formatter instanceof RenderDataInterface) { return $formatter->renderData($originalData, $restructuredData, $options); } return $restructuredData; } /** * Determine if the provided data is compatible with the formatter being used. * * @param FormatterInterface $formatter Formatter being used * @param mixed $structuredOutput Data to validate * @return mixed */ public function validateData(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options) { // If the formatter implements ValidationInterface, then let it // test the data and throw or return an error if ($formatter instanceof ValidationInterface) { return $formatter->validate($structuredOutput); } // If the formatter does not implement ValidationInterface, then // it will never be passed an ArrayObject; we will always give // it a simple array. $structuredOutput = $this->simplifyToArray($structuredOutput, $options); // If we could not simplify to an array, then throw an exception. // We will never give a formatter anything other than an array // unless it validates that it can accept the data type. if (!is_array($structuredOutput)) { throw new IncompatibleDataException( $formatter, $structuredOutput, [] ); } return $structuredOutput; } protected function simplifyToArray($structuredOutput, FormatterOptions $options) { // We can do nothing unless the provided data is an object. if (!is_object($structuredOutput)) { return $structuredOutput; } // Check to see if any of the simplifiers can convert the given data // set to an array. $outputDataType = new \ReflectionClass($structuredOutput); foreach ($this->arraySimplifiers as $simplifier) { if ($simplifier->canSimplify($outputDataType)) { $structuredOutput = $simplifier->simplifyToArray($structuredOutput, $options); } } // Convert data structure back into its original form, if necessary. if ($structuredOutput instanceof OriginalDataInterface) { return $structuredOutput->getOriginalData(); } // Convert \ArrayObjects to a simple array. if ($structuredOutput instanceof \ArrayObject) { return $structuredOutput->getArrayCopy(); } return $structuredOutput; } protected function canSimplifyToArray(\ReflectionClass $structuredOutput) { foreach ($this->arraySimplifiers as $simplifier) { if ($simplifier->canSimplify($structuredOutput)) { return true; } } return false; } /** * Restructure the data as necessary (e.g. to select or reorder fields). * * @param mixed $structuredOutput * @param FormatterOptions $options * @return mixed */ public function restructureData($structuredOutput, FormatterOptions $options) { if ($structuredOutput instanceof RestructureInterface) { return $structuredOutput->restructure($options); } return $structuredOutput; } /** * Allow the formatter access to the raw structured data prior * to restructuring. For example, the 'list' formatter may wish * to display the row keys when provided table output. If this * function returns a result that does not evaluate to 'false', * then that result will be used as-is, and restructuring and * validation will not occur. * * @param mixed $structuredOutput * @param FormatterOptions $options * @return mixed */ public function overrideRestructure(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options) { if ($formatter instanceof OverrideRestructureInterface) { return $formatter->overrideRestructure($structuredOutput, $options); } } /** * Allow the formatter to mess with the configuration options before any * transformations et. al. get underway. * @param FormatterInterface $formatter * @param mixed $structuredOutput * @param FormatterOptions $options * @return FormatterOptions */ public function overrideOptions(FormatterInterface $formatter, $structuredOutput, FormatterOptions $options) { if ($formatter instanceof OverrideOptionsInterface) { return $formatter->overrideOptions($structuredOutput, $options); } return $options; } } validDataTypes() ); } // If the data was provided to us as a single array, then // convert it to a single row. if (is_array($structuredData) && !empty($structuredData)) { $firstRow = reset($structuredData); if (!is_array($firstRow)) { return [$structuredData]; } } return $structuredData; } /** * Return default values for formatter options * @return array */ protected function getDefaultFormatterOptions() { return [ FormatterOptions::INCLUDE_FIELD_LABELS => true, FormatterOptions::DELIMITER => ',', ]; } /** * @inheritdoc */ public function write(OutputInterface $output, $data, FormatterOptions $options) { $defaults = $this->getDefaultFormatterOptions(); $includeFieldLabels = $options->get(FormatterOptions::INCLUDE_FIELD_LABELS, $defaults); if ($includeFieldLabels && ($data instanceof TableTransformation)) { $headers = $data->getHeaders(); $this->writeOneLine($output, $headers, $options); } foreach ($data as $line) { $this->writeOneLine($output, $line, $options); } } protected function writeOneLine(OutputInterface $output, $data, $options) { $defaults = $this->getDefaultFormatterOptions(); $delimiter = $options->get(FormatterOptions::DELIMITER, $defaults); $output->write($this->csvEscape($data, $delimiter)); } protected function csvEscape($data, $delimiter = ',') { $buffer = fopen('php://temp', 'r+'); fputcsv($buffer, $data, $delimiter); rewind($buffer); $csv = fgets($buffer); fclose($buffer); return $csv; } } writeln(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); } } writeln(implode("\n", $data)); } /** * @inheritdoc */ public function overrideRestructure($structuredOutput, FormatterOptions $options) { // If the structured data implements ListDataInterface, // then we will render whatever data its 'getListData' // method provides. if ($structuredOutput instanceof ListDataInterface) { return $this->renderData($structuredOutput, $structuredOutput->getListData($options), $options); } } /** * @inheritdoc */ public function renderData($originalData, $restructuredData, FormatterOptions $options) { if ($originalData instanceof RenderCellInterface) { return $this->renderEachCell($originalData, $restructuredData, $options); } return $restructuredData; } protected function renderEachCell($originalData, $restructuredData, FormatterOptions $options) { foreach ($restructuredData as $key => $cellData) { $restructuredData[$key] = $originalData->renderCell($key, $cellData, $options, $restructuredData); } return $restructuredData; } } writeln(print_r($data, true)); } } renderEachCell($originalData, $restructuredData, $options); } return $restructuredData; } protected function renderEachCell($originalData, $restructuredData, FormatterOptions $options) { foreach ($restructuredData as $id => $row) { foreach ($row as $key => $cellData) { $restructuredData[$id][$key] = $originalData->renderCell($key, $cellData, $options, $row); } } return $restructuredData; } } validDataTypes() ); } return $structuredData; } /** * @inheritdoc */ public function write(OutputInterface $output, $tableTransformer, FormatterOptions $options) { $table = new Table($output); $table->setStyle('compact'); foreach ($tableTransformer as $rowid => $row) { $rowLabel = $tableTransformer->getRowLabel($rowid); $output->writeln(''); $output->writeln($rowLabel); $sectionData = new PropertyList($row); $sectionOptions = new FormatterOptions([], $options->getOptions()); $sectionTableTransformer = $sectionData->restructure($sectionOptions); $table->setRows($sectionTableTransformer->getTableData(true)); $table->render(); } } } writeln(serialize($data)); } } writeln($data); } return $this->reduceToSigleFieldAndWrite($output, $data, $options); } /** * @inheritdoc */ public function overrideOptions($structuredOutput, FormatterOptions $options) { $defaultField = $options->get(FormatterOptions::DEFAULT_STRING_FIELD, [], ''); $userFields = $options->get(FormatterOptions::FIELDS, [FormatterOptions::FIELDS => $options->get(FormatterOptions::FIELD)]); $optionsOverride = $options->override([]); if (empty($userFields) && !empty($defaultField)) { $optionsOverride->setOption(FormatterOptions::FIELDS, $defaultField); } return $optionsOverride; } /** * If the data provided to a 'string' formatter is a table, then try * to emit it as a TSV value. * * @param OutputInterface $output * @param mixed $data * @param FormatterOptions $options */ protected function reduceToSigleFieldAndWrite(OutputInterface $output, $data, FormatterOptions $options) { $alternateFormatter = new TsvFormatter(); try { $data = $alternateFormatter->validate($data); $alternateFormatter->write($output, $data, $options); } catch (\Exception $e) { } } /** * Always validate any data, though. This format will never * cause an error if it is selected for an incompatible data type; at * worse, it simply does not print any data. */ public function validate($structuredData) { return $structuredData; } } validDataTypes() ); } return $structuredData; } /** * @inheritdoc */ public function write(OutputInterface $output, $tableTransformer, FormatterOptions $options) { $headers = []; $defaults = [ FormatterOptions::TABLE_STYLE => 'consolidation', FormatterOptions::INCLUDE_FIELD_LABELS => true, ]; $table = new Table($output); static::addCustomTableStyles($table); $table->setStyle($options->get(FormatterOptions::TABLE_STYLE, $defaults)); $isList = $tableTransformer->isList(); $includeHeaders = $options->get(FormatterOptions::INCLUDE_FIELD_LABELS, $defaults); $listDelimiter = $options->get(FormatterOptions::LIST_DELIMITER, $defaults); $headers = $tableTransformer->getHeaders(); $data = $tableTransformer->getTableData($includeHeaders && $isList); if ($listDelimiter) { if (!empty($headers)) { array_splice($headers, 1, 0, ':'); } $data = array_map(function ($item) { array_splice($item, 1, 0, ':'); return $item; }, $data); } if ($includeHeaders && !$isList) { $table->setHeaders($headers); } // todo: $output->getFormatter(); $data = $this->wrap($headers, $data, $table->getStyle(), $options); $table->setRows($data); $table->render(); } /** * Wrap the table data * @param array $data * @param TableStyle $tableStyle * @param FormatterOptions $options * @return array */ protected function wrap($headers, $data, TableStyle $tableStyle, FormatterOptions $options) { $wrapper = new WordWrapper($options->get(FormatterOptions::TERMINAL_WIDTH)); $wrapper->setPaddingFromStyle($tableStyle); if (!empty($headers)) { $headerLengths = array_map(function ($item) { return strlen($item); }, $headers); $wrapper->setMinimumWidths($headerLengths); } return $wrapper->wrap($data); } /** * Add our custom table style(s) to the table. */ protected static function addCustomTableStyles($table) { // The 'consolidation' style is the same as the 'symfony-style-guide' // style, except it maintains the colored headers used in 'default'. $consolidationStyle = new TableStyle(); $consolidationStyle ->setHorizontalBorderChar('-') ->setVerticalBorderChar(' ') ->setCrossingChar(' ') ; $table->setStyleDefinition('consolidation', $consolidationStyle); } } false, ]; } protected function writeOneLine(OutputInterface $output, $data, $options) { $output->writeln($this->tsvEscape($data)); } protected function tsvEscape($data) { return implode("\t", array_map( function ($item) { return str_replace(["\t", "\n"], ['\t', '\n'], $item); }, $data )); } } writeln(var_export($data, true)); } } getDomData(); } if (!is_array($structuredData)) { throw new IncompatibleDataException( $this, $structuredData, $this->validDataTypes() ); } return $structuredData; } /** * @inheritdoc */ public function write(OutputInterface $output, $dom, FormatterOptions $options) { if (is_array($dom)) { $schema = $options->getXmlSchema(); $dom = $schema->arrayToXML($dom); } $dom->formatOutput = true; $output->writeln($dom->saveXML()); } } writeln(Yaml::dump($data, PHP_INT_MAX, $indent, false, true)); } } configurationData = $configurationData; $this->options = $options; } /** * Create a new FormatterOptions object with new configuration data (provided), * and the same options data as this instance. * * @param array $configurationData * @return FormatterOptions */ public function override($configurationData) { $override = new self(); $override ->setConfigurationData($configurationData + $this->getConfigurationData()) ->setOptions($this->getOptions()); return $override; } public function setTableStyle($style) { return $this->setConfigurationValue(self::TABLE_STYLE, $style); } public function setDelimiter($delimiter) { return $this->setConfigurationValue(self::DELIMITER, $delimiter); } public function setListDelimiter($listDelimiter) { return $this->setConfigurationValue(self::LIST_DELIMITER, $listDelimiter); } public function setIncludeFieldLables($includFieldLables) { return $this->setConfigurationValue(self::INCLUDE_FIELD_LABELS, $includFieldLables); } public function setListOrientation($listOrientation) { return $this->setConfigurationValue(self::LIST_ORIENTATION, $listOrientation); } public function setRowLabels($rowLabels) { return $this->setConfigurationValue(self::ROW_LABELS, $rowLabels); } public function setDefaultFields($fields) { return $this->setConfigurationValue(self::DEFAULT_FIELDS, $fields); } public function setFieldLabels($fieldLabels) { return $this->setConfigurationValue(self::FIELD_LABELS, $fieldLabels); } public function setDefaultStringField($defaultStringField) { return $this->setConfigurationValue(self::DEFAULT_STRING_FIELD, $defaultStringField); } public function setWidth($width) { return $this->setConfigurationValue(self::TERMINAL_WIDTH, $width); } /** * Get a formatter option * * @param string $key * @param array $defaults * @param mixed $default * @return mixed */ public function get($key, $defaults = [], $default = false) { $value = $this->fetch($key, $defaults, $default); return $this->parse($key, $value); } /** * Return the XmlSchema to use with --format=xml for data types that support * that. This is used when an array needs to be converted into xml. * * @return XmlSchema */ public function getXmlSchema() { return new XmlSchema(); } /** * Determine the format that was requested by the caller. * * @param array $defaults * @return string */ public function getFormat($defaults = []) { return $this->get(self::FORMAT, [], $this->get(self::DEFAULT_FORMAT, $defaults, '')); } /** * Look up a key, and return its raw value. * * @param string $key * @param array $defaults * @param mixed $default * @return mixed */ protected function fetch($key, $defaults = [], $default = false) { $defaults = $this->defaultsForKey($key, $defaults, $default); $values = $this->fetchRawValues($defaults); return $values[$key]; } /** * Reduce provided defaults to the single item identified by '$key', * if it exists, or an empty array otherwise. * * @param string $key * @param array $defaults * @return array */ protected function defaultsForKey($key, $defaults, $default = false) { if (array_key_exists($key, $defaults)) { return [$key => $defaults[$key]]; } return [$key => $default]; } /** * Look up all of the items associated with the provided defaults. * * @param array $defaults * @return array */ protected function fetchRawValues($defaults = []) { return array_merge( $defaults, $this->getConfigurationData(), $this->getOptions(), $this->getInputOptions($defaults) ); } /** * Given the raw value for a specific key, do any type conversion * (e.g. from a textual list to an array) needed for the data. * * @param string $key * @param mixed $value * @return mixed */ protected function parse($key, $value) { $optionFormat = $this->getOptionFormat($key); if (!empty($optionFormat) && is_string($value)) { return $this->$optionFormat($value); } return $value; } /** * Convert from a textual list to an array * * @param string $value * @return array */ public function parsePropertyList($value) { return PropertyParser::parse($value); } /** * Given a specific key, return the class method name of the * parsing method for data stored under this key. * * @param string $key * @return string */ protected function getOptionFormat($key) { $propertyFormats = [ self::ROW_LABELS => 'PropertyList', self::FIELD_LABELS => 'PropertyList', ]; if (array_key_exists($key, $propertyFormats)) { return "parse{$propertyFormats[$key]}"; } return ''; } /** * Change the configuration data for this formatter options object. * * @param array $configurationData * @return FormatterOptions */ public function setConfigurationData($configurationData) { $this->configurationData = $configurationData; return $this; } /** * Change one configuration value for this formatter option. * * @param string $key * @param mixed $value * @return FormetterOptions */ protected function setConfigurationValue($key, $value) { $this->configurationData[$key] = $value; return $this; } /** * Change one configuration value for this formatter option, but only * if it does not already have a value set. * * @param string $key * @param mixed $value * @return FormetterOptions */ public function setConfigurationDefault($key, $value) { if (!array_key_exists($key, $this->configurationData)) { return $this->setConfigurationValue($key, $value); } return $this; } /** * Return a reference to the configuration data for this object. * * @return array */ public function getConfigurationData() { return $this->configurationData; } /** * Set all of the options that were specified by the user for this request. * * @param array $options * @return FormatterOptions */ public function setOptions($options) { $this->options = $options; return $this; } /** * Change one option value specified by the user for this request. * * @param string $key * @param mixed $value * @return FormatterOptions */ public function setOption($key, $value) { $this->options[$key] = $value; return $this; } /** * Return a reference to the user-specified options for this request. * * @return array */ public function getOptions() { return $this->options; } /** * Provide a Symfony Console InputInterface containing the user-specified * options for this request. * * @param InputInterface $input * @return type */ public function setInput(InputInterface $input) { $this->input = $input; } /** * Return all of the options from the provided $defaults array that * exist in our InputInterface object. * * @param array $defaults * @return array */ public function getInputOptions($defaults) { if (!isset($this->input)) { return []; } $options = []; foreach ($defaults as $key => $value) { if ($this->input->hasOption($key)) { $result = $this->input->getOption($key); if (isset($result)) { $options[$key] = $this->input->getOption($key); } } } return $options; } } defaultOptions(); $fieldLabels = $this->getReorderedFieldLabels($data, $options, $defaults); $tableTransformer = $this->instantiateTableTransformation($data, $fieldLabels, $options->get(FormatterOptions::ROW_LABELS, $defaults)); if ($options->get(FormatterOptions::LIST_ORIENTATION, $defaults)) { $tableTransformer->setLayout(TableTransformation::LIST_LAYOUT); } return $tableTransformer; } protected function instantiateTableTransformation($data, $fieldLabels, $rowLabels) { return new TableTransformation($data, $fieldLabels, $rowLabels); } protected function getReorderedFieldLabels($data, $options, $defaults) { $reorderer = new ReorderFields(); $fieldLabels = $reorderer->reorder( $this->getFields($options, $defaults), $options->get(FormatterOptions::FIELD_LABELS, $defaults), $data ); return $fieldLabels; } protected function getFields($options, $defaults) { $fieldShortcut = $options->get(FormatterOptions::FIELD); if (!empty($fieldShortcut)) { return [$fieldShortcut]; } $result = $options->get(FormatterOptions::FIELDS, $defaults); if (!empty($result)) { return $result; } return $options->get(FormatterOptions::DEFAULT_FIELDS, $defaults); } /** * A structured list may provide its own set of default options. These * will be used in place of the command's default options (from the * annotations) in instances where the user does not provide the options * explicitly (on the commandline) or implicitly (via a configuration file). * * @return array */ protected function defaultOptions() { return [ FormatterOptions::FIELDS => [], FormatterOptions::FIELD_LABELS => [], FormatterOptions::ROW_LABELS => [], FormatterOptions::DEFAULT_FIELDS => [], ]; } } renderFunction = $renderFunction; } /** * {@inheritdoc} */ public function renderCell($key, $cellData, FormatterOptions $options, $rowData) { return call_user_func($this->renderFunction, $key, $cellData, $options, $rowData); } } getArrayCopy()]; $options->setConfigurationDefault('list-orientation', true); $tableTransformer = $this->createTableTransformation($data, $options); return $tableTransformer; } public function getListData(FormatterOptions $options) { $data = $this->getArrayCopy(); $defaults = $this->defaultOptions(); $fieldLabels = $this->getReorderedFieldLabels([$data], $options, $defaults); $result = []; foreach ($fieldLabels as $id => $label) { $result[$id] = $data[$id]; } return $result; } protected function defaultOptions() { return [ FormatterOptions::LIST_ORIENTATION => true, ] + parent::defaultOptions(); } protected function instantiateTableTransformation($data, $fieldLabels, $rowLabels) { return new PropertyListTableTransformation($data, $fieldLabels, $rowLabels); } } [], RenderCellCollectionInterface::PRIORITY_NORMAL => [], RenderCellCollectionInterface::PRIORITY_FALLBACK => [], ]; /** * Add a renderer * * @return $this */ public function addRenderer(RenderCellInterface $renderer, $priority = RenderCellCollectionInterface::PRIORITY_NORMAL) { $this->rendererList[$priority][] = $renderer; return $this; } /** * Add a callable as a renderer * * @return $this */ public function addRendererFunction(callable $rendererFn, $priority = RenderCellCollectionInterface::PRIORITY_NORMAL) { $renderer = new CallableRenderer($rendererFn); return $this->addRenderer($renderer, $priority); } /** * {@inheritdoc} */ public function renderCell($key, $cellData, FormatterOptions $options, $rowData) { $flattenedRendererList = array_reduce( $this->rendererList, function ($carry, $item) { return array_merge($carry, $item); }, [] ); foreach ($flattenedRendererList as $renderer) { $cellData = $renderer->renderCell($key, $cellData, $options, $rowData); if (is_string($cellData)) { return $cellData; } } return $cellData; } } getArrayCopy(); return $this->createTableTransformation($data, $options); } public function getListData(FormatterOptions $options) { return array_keys($this->getArrayCopy()); } protected function defaultOptions() { return [ FormatterOptions::LIST_ORIENTATION => false, ] + parent::defaultOptions(); } } ['description'], ]; $this->elementList = array_merge_recursive($elementList, $defaultElementList); } public function arrayToXML($structuredData) { $dom = new \DOMDocument('1.0', 'UTF-8'); $topLevelElement = $this->getTopLevelElementName($structuredData); $this->addXmlData($dom, $dom, $topLevelElement, $structuredData); return $dom; } protected function addXmlData(\DOMDocument $dom, $xmlParent, $elementName, $structuredData) { $element = $dom->createElement($elementName); $xmlParent->appendChild($element); if (is_string($structuredData)) { $element->appendChild($dom->createTextNode($structuredData)); return; } $this->addXmlChildren($dom, $element, $elementName, $structuredData); } protected function addXmlChildren(\DOMDocument $dom, $xmlParent, $elementName, $structuredData) { foreach ($structuredData as $key => $value) { $this->addXmlDataOrAttribute($dom, $xmlParent, $elementName, $key, $value); } } protected function addXmlDataOrAttribute(\DOMDocument $dom, $xmlParent, $elementName, $key, $value) { $childElementName = $this->getDefaultElementName($elementName); $elementName = $this->determineElementName($key, $childElementName, $value); if (($elementName != $childElementName) && $this->isAttribute($elementName, $key, $value)) { $xmlParent->setAttribute($key, $value); return; } $this->addXmlData($dom, $xmlParent, $elementName, $value); } protected function determineElementName($key, $childElementName, $value) { if (is_numeric($key)) { return $childElementName; } if (is_object($value)) { $value = (array)$value; } if (!is_array($value)) { return $key; } if (array_key_exists('id', $value) && ($value['id'] == $key)) { return $childElementName; } if (array_key_exists('name', $value) && ($value['name'] == $key)) { return $childElementName; } return $key; } protected function getTopLevelElementName($structuredData) { return 'document'; } protected function getDefaultElementName($parentElementName) { $singularName = $this->singularForm($parentElementName); if (isset($singularName)) { return $singularName; } return 'item'; } protected function isAttribute($parentElementName, $elementName, $value) { if (!is_string($value)) { return false; } return !$this->inElementList($parentElementName, $elementName) && !$this->inElementList('*', $elementName); } protected function inElementList($parentElementName, $elementName) { if (!array_key_exists($parentElementName, $this->elementList)) { return false; } return in_array($elementName, $this->elementList[$parentElementName]); } protected function singularForm($name) { if (substr($name, strlen($name) - 1) == "s") { return substr($name, 0, strlen($name) - 1); } } protected function isAssoc($data) { return array_keys($data) == range(0, count($data)); } } * * * blah * * * a * b * c * * * * * * * This could be: * * [ * 'id' => 1, * 'name' => 'doc', * 'foobars' => * [ * [ * 'id' => '123', * 'name' => 'blah', * 'widgets' => * [ * [ * 'foo' => 'a', * 'bar' => 'b', * 'baz' => 'c', * ] * ], * ], * ] * ] * * The challenge is more in going from an array back to the more * structured xml format. Note that any given key => string mapping * could represent either an attribute, or a simple XML element * containing only a string value. In general, we do *not* want to add * extra layers of nesting in the data structure to disambiguate between * these kinds of data, as we want the source data to render cleanly * into other formats, e.g. yaml, json, et. al., and we do not want to * force every data provider to have to consider the optimal xml schema * for their data. * * Our strategy, therefore, is to expect clients that wish to provide * a very specific xml representation to return a DOMDocument, and, * for other data structures where xml is a secondary concern, then we * will use some default heuristics to convert from arrays to xml. */ interface XmlSchemaInterface { /** * Convert data to a format suitable for use in a list. * By default, the array values will be used. Implement * ListDataInterface to use some other criteria (e.g. array keys). * * @return \DOMDocument */ public function arrayToXml($structuredData); } isSubclassOf('\Consolidation\OutputFormatters\StructuredData\Xml\DomDataInterface') || $dataType->isSubclassOf('DOMDocument') || ($dataType->getName() == 'DOMDocument'); } public function simplifyToArray($structuredData, FormatterOptions $options) { if ($structuredData instanceof DomDataInterface) { $structuredData = $structuredData->getDomData(); } if ($structuredData instanceof \DOMDocument) { // $schema = $options->getXmlSchema(); $simplified = $this->elementToArray($structuredData); $structuredData = array_shift($simplified); } return $structuredData; } /** * Recursively convert the provided DOM element into a php array. * * @param \DOMNode $element * @return array */ protected function elementToArray(\DOMNode $element) { if ($element->nodeType == XML_TEXT_NODE) { return $element->nodeValue; } $attributes = $this->getNodeAttributes($element); $children = $this->getNodeChildren($element); return array_merge($attributes, $children); } /** * Get all of the attributes of the provided element. * * @param \DOMNode $element * @return array */ protected function getNodeAttributes($element) { if (empty($element->attributes)) { return []; } $attributes = []; foreach ($element->attributes as $key => $attribute) { $attributes[$key] = $attribute->nodeValue; } return $attributes; } /** * Get all of the children of the provided element, with simplification. * * @param \DOMNode $element * @return array */ protected function getNodeChildren($element) { if (empty($element->childNodes)) { return []; } $uniformChildrenName = $this->hasUniformChildren($element); if ("{$uniformChildrenName}s" == $element->nodeName) { $result = $this->getUniformChildren($element->nodeName, $element); } else { $result = $this->getUniqueChildren($element->nodeName, $element); } return array_filter($result); } /** * Get the data from the children of the provided node in preliminary * form. * * @param \DOMNode $element * @return array */ protected function getNodeChildrenData($element) { $children = []; foreach ($element->childNodes as $key => $value) { $children[$key] = $this->elementToArray($value); } return $children; } /** * Determine whether the children of the provided element are uniform. * @see getUniformChildren(), below. * * @param \DOMNode $element * @return boolean */ protected function hasUniformChildren($element) { $last = false; foreach ($element->childNodes as $key => $value) { $name = $value->nodeName; if (!$name) { return false; } if ($last && ($name != $last)) { return false; } $last = $name; } return $last; } /** * Convert the children of the provided DOM element into an array. * Here, 'uniform' means that all of the element names of the children * are identical, and further, the element name of the parent is the * plural form of the child names. When the children are uniform in * this way, then the parent element name will be used as the key to * store the children in, and the child list will be returned as a * simple list with their (duplicate) element names omitted. * * @param string $parentKey * @param \DOMNode $element * @return array */ protected function getUniformChildren($parentKey, $element) { $children = $this->getNodeChildrenData($element); $simplifiedChildren = []; foreach ($children as $key => $value) { if ($this->valueCanBeSimplified($value)) { $value = array_shift($value); } $id = $this->getIdOfValue($value); if ($id) { $simplifiedChildren[$parentKey][$id] = $value; } else { $simplifiedChildren[$parentKey][] = $value; } } return $simplifiedChildren; } /** * Determine whether the provided value has additional unnecessary * nesting. {"color": "red"} is converted to "red". No other * simplification is done. * * @param \DOMNode $value * @return boolean */ protected function valueCanBeSimplified($value) { if (!is_array($value)) { return false; } if (count($value) != 1) { return false; } $data = array_shift($value); return is_string($data); } /** * If the object has an 'id' or 'name' element, then use that * as the array key when storing this value in its parent. * @param mixed $value * @return string */ protected function getIdOfValue($value) { if (!is_array($value)) { return false; } if (array_key_exists('id', $value)) { return trim($value['id'], '-'); } if (array_key_exists('name', $value)) { return trim($value['name'], '-'); } } /** * Convert the children of the provided DOM element into an array. * Here, 'unique' means that all of the element names of the children are * different. Since the element names will become the key of the * associative array that is returned, so duplicates are not supported. * If there are any duplicates, then an exception will be thrown. * * @param string $parentKey * @param \DOMNode $element * @return array */ protected function getUniqueChildren($parentKey, $element) { $children = $this->getNodeChildrenData($element); if ((count($children) == 1) && (is_string($children[0]))) { return [$element->nodeName => $children[0]]; } $simplifiedChildren = []; foreach ($children as $key => $value) { if (is_numeric($key) && is_array($value) && (count($value) == 1)) { $valueKeys = array_keys($value); $key = $valueKeys[0]; $value = array_shift($value); } if (array_key_exists($key, $simplifiedChildren)) { throw new \Exception("Cannot convert data from a DOM document to an array, because <$key> appears more than once, and is not wrapped in a <{$key}s> element."); } $simplifiedChildren[$key] = $value; } return $simplifiedChildren; } } getArrayCopy(); return $data[0]; } } 'red', * 'two' => 'white', * 'three' => 'blue', * ] */ class PropertyParser { public static function parse($data) { if (!is_string($data)) { return $data; } $result = []; $lines = explode("\n", $data); foreach ($lines as $line) { list($key, $value) = explode(':', trim($line), 2) + ['', '']; $result[$key] = trim($value); } return $result; } } getSelectedFieldKeys($fields, $fieldLabels); if (empty($fields)) { return array_intersect_key($fieldLabels, $firstRow); } return $this->reorderFieldLabels($fields, $fieldLabels, $data); } protected function reorderFieldLabels($fields, $fieldLabels, $data) { $result = []; $firstRow = reset($data); foreach ($fields as $field) { if (array_key_exists($field, $firstRow)) { if (array_key_exists($field, $fieldLabels)) { $result[$field] = $fieldLabels[$field]; } } } return $result; } protected function getSelectedFieldKeys($fields, $fieldLabels) { if (is_string($fields)) { $fields = explode(',', $fields); } $selectedFields = []; foreach ($fields as $field) { $matchedFields = $this->matchFieldInLabelMap($field, $fieldLabels); if (empty($matchedFields)) { throw new UnknownFieldException($field); } $selectedFields = array_merge($selectedFields, $matchedFields); } return $selectedFields; } protected function matchFieldInLabelMap($field, $fieldLabels) { $fieldRegex = $this->convertToRegex($field); return array_filter( array_keys($fieldLabels), function ($key) use ($fieldRegex, $fieldLabels) { $value = $fieldLabels[$key]; return preg_match($fieldRegex, $value) || preg_match($fieldRegex, $key); } ); } /** * Convert the provided string into a regex suitable for use in * preg_match. * * Matching occurs in the same way as the Symfony Finder component: * http://symfony.com/doc/current/components/finder.html#file-name */ protected function convertToRegex($str) { return $this->isRegex($str) ? $str : Glob::toRegex($str); } /** * Checks whether the string is a regex. This function is copied from * MultiplePcreFilterIterator in the Symfony Finder component. * * @param string $str * * @return bool Whether the given string is a regex */ protected function isRegex($str) { if (preg_match('/^(.{3,}?)[imsxuADU]*$/', $str, $m)) { $start = substr($m[1], 0, 1); $end = substr($m[1], -1); if ($start === $end) { return !preg_match('/[*?[:alnum:] \\\\]/', $start); } foreach (array(array('{', '}'), array('(', ')'), array('[', ']'), array('<', '>')) as $delimiters) { if ($start === $delimiters[0] && $end === $delimiters[1]) { return true; } } } return false; } } headers = $fieldLabels; $this->rowLabels = $rowLabels; $rows = static::transformRows($data, $fieldLabels); $this->layout = self::TABLE_LAYOUT; parent::__construct($rows); } public function setLayout($layout) { $this->layout = $layout; } public function getLayout() { return $this->layout; } public function isList() { return $this->layout == self::LIST_LAYOUT; } protected static function transformRows($data, $fieldLabels) { $rows = []; foreach ($data as $rowid => $row) { $rows[$rowid] = static::transformRow($row, $fieldLabels); } return $rows; } protected static function transformRow($row, $fieldLabels) { $result = []; foreach ($fieldLabels as $key => $label) { $result[$key] = array_key_exists($key, $row) ? $row[$key] : ''; } return $result; } public function getHeaders() { return $this->headers; } public function getHeader($key) { if (array_key_exists($key, $this->headers)) { return $this->headers[$key]; } return $key; } public function getRowLabels() { return $this->rowLabels; } public function getRowLabel($rowid) { if (array_key_exists($rowid, $this->rowLabels)) { return $this->rowLabels[$rowid]; } return $rowid; } public function getOriginalData() { return $this->getArrayCopy(); } public function getTableData($includeRowKey = false) { $data = $this->getArrayCopy(); if ($this->isList()) { $data = $this->convertTableToList(); } if ($includeRowKey) { $data = $this->getRowDataWithKey($data); } return $data; } protected function convertTableToList() { $result = []; foreach ($this as $row) { foreach ($row as $key => $value) { $result[$key][] = $value; } } return $result; } protected function getRowDataWithKey($data) { $result = []; $i = 0; foreach ($data as $key => $row) { array_unshift($row, $this->getHeader($key)); $i++; $result[$key] = $row; } return $result; } } width = $width; } /** * Calculate our padding widths from the specified table style. * @param TableStyle $style */ public function setPaddingFromStyle(TableStyle $style) { $verticalBorderLen = strlen(sprintf($style->getBorderFormat(), $style->getVerticalBorderChar())); $paddingLen = strlen($style->getPaddingChar()); $this->extraPaddingAtBeginningOfLine = 0; $this->extraPaddingAtEndOfLine = $verticalBorderLen; $this->paddingInEachCell = $verticalBorderLen + $paddingLen + 1; } /** * If columns have minimum widths, then set them here. * @param array $minimumWidths */ public function setMinimumWidths($minimumWidths) { $this->minimumWidths = $minimumWidths; } /** * Wrap the cells in each part of the provided data table * @param array $rows * @return array */ public function wrap($rows, $widths = []) { // If the width was not set, then disable wordwrap. if (!$this->width) { return $rows; } // Calculate the column widths to use based on the content. $auto_widths = $this->columnAutowidth($rows, $widths); // Do wordwrap on all cells. $newrows = array(); foreach ($rows as $rowkey => $row) { foreach ($row as $colkey => $cell) { $newrows[$rowkey][$colkey] = $this->wrapCell($cell, $auto_widths[$colkey]); } } return $newrows; } /** * Wrap one cell. Guard against modifying non-strings and * then call through to wordwrap(). * * @param mixed $cell * @param string $cellWidth * @return mixed */ protected function wrapCell($cell, $cellWidth) { if (!is_string($cell)) { return $cell; } return wordwrap($cell, $cellWidth, "\n", true); } /** * Determine the best fit for column widths. Ported from Drush. * * @param array $rows The rows to use for calculations. * @param array $widths Manually specified widths of each column * (in characters) - these will be left as is. */ protected function columnAutowidth($rows, $widths) { $auto_widths = $widths; // First we determine the distribution of row lengths in each column. // This is an array of descending character length keys (i.e. starting at // the rightmost character column), with the value indicating the number // of rows where that character column is present. $col_dist = []; // We will also calculate the longest word in each column $max_word_lens = []; foreach ($rows as $rowkey => $row) { foreach ($row as $col_id => $cell) { $longest_word_len = static::longestWordLength($cell); if ((!isset($max_word_lens[$col_id]) || ($max_word_lens[$col_id] < $longest_word_len))) { $max_word_lens[$col_id] = $longest_word_len; } if (empty($widths[$col_id])) { $length = strlen($cell); if ($length == 0) { $col_dist[$col_id][0] = 0; } while ($length > 0) { if (!isset($col_dist[$col_id][$length])) { $col_dist[$col_id][$length] = 0; } $col_dist[$col_id][$length]++; $length--; } } } } foreach ($col_dist as $col_id => $count) { // Sort the distribution in decending key order. krsort($col_dist[$col_id]); // Initially we set all columns to their "ideal" longest width // - i.e. the width of their longest column. $auto_widths[$col_id] = max(array_keys($col_dist[$col_id])); } // We determine what width we have available to use, and what width the // above "ideal" columns take up. $available_width = $this->width - ($this->extraPaddingAtBeginningOfLine + $this->extraPaddingAtEndOfLine + (count($auto_widths) * $this->paddingInEachCell)); $auto_width_current = array_sum($auto_widths); // If we cannot fit into the minimum width anyway, then just return // the max word length of each column as the 'ideal' $minimumIdealLength = array_sum($this->minimumWidths); if ($minimumIdealLength && ($available_width < $minimumIdealLength)) { return $max_word_lens; } // If we need to reduce a column so that we can fit the space we use this // loop to figure out which column will cause the "least wrapping", // (relative to the other columns) and reduce the width of that column. while ($auto_width_current > $available_width) { list($column, $width) = $this->selectColumnToReduce($col_dist, $auto_widths, $max_word_lens); if (!$column || $width <= 1) { // If we have reached a width of 1 then give up, so wordwrap can still progress. break; } // Reduce the width of the selected column. $auto_widths[$column]--; // Reduce our overall table width counter. $auto_width_current--; // Remove the corresponding data from the disctribution, so next time // around we use the data for the row to the left. unset($col_dist[$column][$width]); } return $auto_widths; } protected function selectColumnToReduce($col_dist, $auto_widths, $max_word_lens) { $column = false; $count = 0; $width = 0; foreach ($col_dist as $col_id => $counts) { // Of the columns whose length is still > than the the lenght // of their maximum word length if ($auto_widths[$col_id] > $max_word_lens[$col_id]) { if ($this->shouldSelectThisColumn($count, $counts, $width)) { $column = $col_id; $count = current($counts); $width = key($counts); } } } if ($column !== false) { return [$column, $width]; } foreach ($col_dist as $col_id => $counts) { if (empty($this->minimumWidths) || ($auto_widths[$col_id] > $this->minimumWidths[$col_id])) { if ($this->shouldSelectThisColumn($count, $counts, $width)) { $column = $col_id; $count = current($counts); $width = key($counts); } } } return [$column, $width]; } protected function shouldSelectThisColumn($count, $counts, $width) { return // If we are just starting out, select the first column. ($count == 0) || // OR: if this column would cause less wrapping than the currently // selected column, then select it. (current($counts) < $count) || // OR: if this column would cause the same amount of wrapping, but is // longer, then we choose to wrap the longer column (proportionally // less wrapping, and helps avoid triple line wraps). (current($counts) == $count && key($counts) > $width); } /** * Return the length of the longest word in the string. * @param string $str * @return int */ protected static function longestWordLength($str) { $words = preg_split('/[ -]/', $str); $lengths = array_map(function ($s) { return strlen($s); }, $words); return max($lengths); } } validDataTypes(), function ($carry, $supportedType) use ($dataType) { return $carry || ($dataType->getName() == $supportedType->getName()) || ($dataType->isSubclassOf($supportedType->getName())); }, false ); } } getHomeDir() . DIRECTORY_SEPARATOR . '.config'; return $path; } /** * @return string */ public function getHomeDataDir() { $path = getenv('XDG_DATA_HOME') ?: $this->getHomeDir() . DIRECTORY_SEPARATOR . '.local' . DIRECTORY_SEPARATOR . 'share'; return $path; } /** * @return array */ public function getConfigDirs() { $configDirs = getenv('XDG_CONFIG_DIRS') ? explode(':', getenv('XDG_CONFIG_DIRS')) : array('/etc/xdg'); $paths = array_merge(array($this->getHomeConfigDir()), $configDirs); return $paths; } /** * @return array */ public function getDataDirs() { $dataDirs = getenv('XDG_DATA_DIRS') ? explode(':', getenv('XDG_DATA_DIRS')) : array('/usr/local/share', '/usr/share'); $paths = array_merge(array($this->getHomeDataDir()), $dataDirs); return $paths; } /** * @return string */ public function getHomeCacheDir() { $path = getenv('XDG_CACHE_HOME') ?: $this->getHomeDir() . DIRECTORY_SEPARATOR . '.cache'; return $path; } public function getRuntimeDir($strict=true) { if ($runtimeDir = getenv('XDG_RUNTIME_DIR')) { return $runtimeDir; } if ($strict) { throw new \RuntimeException('XDG_RUNTIME_DIR was not set'); } $fallback = sys_get_temp_dir() . DIRECTORY_SEPARATOR . self::RUNTIME_DIR_FALLBACK . getenv('USER'); $create = false; if (!is_dir($fallback)) { mkdir($fallback, 0700, true); } $st = lstat($fallback); # The fallback must be a directory if (!$st['mode'] & self::S_IFDIR) { rmdir($fallback); $create = true; } elseif ($st['uid'] != getmyuid() || $st['mode'] & (self::S_IRWXG | self::S_IRWXO) ) { rmdir($fallback); $create = true; } if ($create) { mkdir($fallback, 0700, true); } return $fallback; } } . */ namespace Doctrine\Instantiator\Exception; /** * Base exception marker interface for the instantiator component * * @author Marco Pivetta */ interface ExceptionInterface { } . */ namespace Doctrine\Instantiator\Exception; use InvalidArgumentException as BaseInvalidArgumentException; use ReflectionClass; /** * Exception for invalid arguments provided to the instantiator * * @author Marco Pivetta */ class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface { /** * @param string $className * * @return self */ public static function fromNonExistingClass($className) { if (interface_exists($className)) { return new self(sprintf('The provided type "%s" is an interface, and can not be instantiated', $className)); } if (PHP_VERSION_ID >= 50400 && trait_exists($className)) { return new self(sprintf('The provided type "%s" is a trait, and can not be instantiated', $className)); } return new self(sprintf('The provided class "%s" does not exist', $className)); } /** * @param ReflectionClass $reflectionClass * * @return self */ public static function fromAbstractClass(ReflectionClass $reflectionClass) { return new self(sprintf( 'The provided class "%s" is abstract, and can not be instantiated', $reflectionClass->getName() )); } } . */ namespace Doctrine\Instantiator\Exception; use Exception; use ReflectionClass; use UnexpectedValueException as BaseUnexpectedValueException; /** * Exception for given parameters causing invalid/unexpected state on instantiation * * @author Marco Pivetta */ class UnexpectedValueException extends BaseUnexpectedValueException implements ExceptionInterface { /** * @param ReflectionClass $reflectionClass * @param Exception $exception * * @return self */ public static function fromSerializationTriggeredException(ReflectionClass $reflectionClass, Exception $exception) { return new self( sprintf( 'An exception was raised while trying to instantiate an instance of "%s" via un-serialization', $reflectionClass->getName() ), 0, $exception ); } /** * @param ReflectionClass $reflectionClass * @param string $errorString * @param int $errorCode * @param string $errorFile * @param int $errorLine * * @return UnexpectedValueException */ public static function fromUncleanUnSerialization( ReflectionClass $reflectionClass, $errorString, $errorCode, $errorFile, $errorLine ) { return new self( sprintf( 'Could not produce an instance of "%s" via un-serialization, since an error was triggered ' . 'in file "%s" at line "%d"', $reflectionClass->getName(), $errorFile, $errorLine ), 0, new Exception($errorString, $errorCode) ); } } . */ namespace Doctrine\Instantiator; use Closure; use Doctrine\Instantiator\Exception\InvalidArgumentException; use Doctrine\Instantiator\Exception\UnexpectedValueException; use Exception; use ReflectionClass; /** * {@inheritDoc} * * @author Marco Pivetta */ final class Instantiator implements InstantiatorInterface { /** * Markers used internally by PHP to define whether {@see \unserialize} should invoke * the method {@see \Serializable::unserialize()} when dealing with classes implementing * the {@see \Serializable} interface. */ const SERIALIZATION_FORMAT_USE_UNSERIALIZER = 'C'; const SERIALIZATION_FORMAT_AVOID_UNSERIALIZER = 'O'; /** * @var \Closure[] of {@see \Closure} instances used to instantiate specific classes */ private static $cachedInstantiators = array(); /** * @var object[] of objects that can directly be cloned */ private static $cachedCloneables = array(); /** * {@inheritDoc} */ public function instantiate($className) { if (isset(self::$cachedCloneables[$className])) { return clone self::$cachedCloneables[$className]; } if (isset(self::$cachedInstantiators[$className])) { $factory = self::$cachedInstantiators[$className]; return $factory(); } return $this->buildAndCacheFromFactory($className); } /** * Builds the requested object and caches it in static properties for performance * * @param string $className * * @return object */ private function buildAndCacheFromFactory($className) { $factory = self::$cachedInstantiators[$className] = $this->buildFactory($className); $instance = $factory(); if ($this->isSafeToClone(new ReflectionClass($instance))) { self::$cachedCloneables[$className] = clone $instance; } return $instance; } /** * Builds a {@see \Closure} capable of instantiating the given $className without * invoking its constructor. * * @param string $className * * @return Closure */ private function buildFactory($className) { $reflectionClass = $this->getReflectionClass($className); if ($this->isInstantiableViaReflection($reflectionClass)) { return function () use ($reflectionClass) { return $reflectionClass->newInstanceWithoutConstructor(); }; } $serializedString = sprintf( '%s:%d:"%s":0:{}', $this->getSerializationFormat($reflectionClass), strlen($className), $className ); $this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString); return function () use ($serializedString) { return unserialize($serializedString); }; } /** * @param string $className * * @return ReflectionClass * * @throws InvalidArgumentException */ private function getReflectionClass($className) { if (! class_exists($className)) { throw InvalidArgumentException::fromNonExistingClass($className); } $reflection = new ReflectionClass($className); if ($reflection->isAbstract()) { throw InvalidArgumentException::fromAbstractClass($reflection); } return $reflection; } /** * @param ReflectionClass $reflectionClass * @param string $serializedString * * @throws UnexpectedValueException * * @return void */ private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, $serializedString) { set_error_handler(function ($code, $message, $file, $line) use ($reflectionClass, & $error) { $error = UnexpectedValueException::fromUncleanUnSerialization( $reflectionClass, $message, $code, $file, $line ); }); $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString); restore_error_handler(); if ($error) { throw $error; } } /** * @param ReflectionClass $reflectionClass * @param string $serializedString * * @throws UnexpectedValueException * * @return void */ private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, $serializedString) { try { unserialize($serializedString); } catch (Exception $exception) { restore_error_handler(); throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception); } } /** * @param ReflectionClass $reflectionClass * * @return bool */ private function isInstantiableViaReflection(ReflectionClass $reflectionClass) { if (\PHP_VERSION_ID >= 50600) { return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal()); } return \PHP_VERSION_ID >= 50400 && ! $this->hasInternalAncestors($reflectionClass); } /** * Verifies whether the given class is to be considered internal * * @param ReflectionClass $reflectionClass * * @return bool */ private function hasInternalAncestors(ReflectionClass $reflectionClass) { do { if ($reflectionClass->isInternal()) { return true; } } while ($reflectionClass = $reflectionClass->getParentClass()); return false; } /** * Verifies if the given PHP version implements the `Serializable` interface serialization * with an incompatible serialization format. If that's the case, use serialization marker * "C" instead of "O". * * @link http://news.php.net/php.internals/74654 * * @param ReflectionClass $reflectionClass * * @return string the serialization format marker, either self::SERIALIZATION_FORMAT_USE_UNSERIALIZER * or self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER */ private function getSerializationFormat(ReflectionClass $reflectionClass) { if ($this->isPhpVersionWithBrokenSerializationFormat() && $reflectionClass->implementsInterface('Serializable') ) { return self::SERIALIZATION_FORMAT_USE_UNSERIALIZER; } return self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER; } /** * Checks whether the current PHP runtime uses an incompatible serialization format * * @return bool */ private function isPhpVersionWithBrokenSerializationFormat() { return PHP_VERSION_ID === 50429 || PHP_VERSION_ID === 50513; } /** * Checks if a class is cloneable * * @param ReflectionClass $reflection * * @return bool */ private function isSafeToClone(ReflectionClass $reflection) { if (method_exists($reflection, 'isCloneable') && ! $reflection->isCloneable()) { return false; } // not cloneable if it implements `__clone`, as we want to avoid calling it return ! $reflection->hasMethod('__clone'); } } . */ namespace Doctrine\Instantiator; /** * Instantiator provides utility methods to build objects without invoking their constructors * * @author Marco Pivetta */ interface InstantiatorInterface { /** * @param string $className * * @return object * * @throws \Doctrine\Instantiator\Exception\ExceptionInterface */ public function instantiate($className); } isSupported() ? 'Yes' : 'No') . "\n"; echo "256 colors are supported: " . ($consoleColor->are256ColorsSupported() ? 'Yes' : 'No') . "\n\n"; if ($consoleColor->isSupported()) { foreach ($consoleColor->getPossibleStyles() as $style) { echo $consoleColor->apply($style, $style) . "\n"; } } echo "\n"; if ($consoleColor->are256ColorsSupported()) { echo "Foreground colors:\n"; for ($i = 1; $i <= 255; $i++) { echo $consoleColor->apply("color_$i", str_pad($i, 6, ' ', STR_PAD_BOTH)); if ($i % 15 === 0) { echo "\n"; } } echo "\nBackground colors:\n"; for ($i = 1; $i <= 255; $i++) { echo $consoleColor->apply("bg_color_$i", str_pad($i, 6, ' ', STR_PAD_BOTH)); if ($i % 15 === 0) { echo "\n"; } } echo "\n"; } null, 'bold' => '1', 'dark' => '2', 'italic' => '3', 'underline' => '4', 'blink' => '5', 'reverse' => '7', 'concealed' => '8', 'default' => '39', 'black' => '30', 'red' => '31', 'green' => '32', 'yellow' => '33', 'blue' => '34', 'magenta' => '35', 'cyan' => '36', 'light_gray' => '37', 'dark_gray' => '90', 'light_red' => '91', 'light_green' => '92', 'light_yellow' => '93', 'light_blue' => '94', 'light_magenta' => '95', 'light_cyan' => '96', 'white' => '97', 'bg_default' => '49', 'bg_black' => '40', 'bg_red' => '41', 'bg_green' => '42', 'bg_yellow' => '43', 'bg_blue' => '44', 'bg_magenta' => '45', 'bg_cyan' => '46', 'bg_light_gray' => '47', 'bg_dark_gray' => '100', 'bg_light_red' => '101', 'bg_light_green' => '102', 'bg_light_yellow' => '103', 'bg_light_blue' => '104', 'bg_light_magenta' => '105', 'bg_light_cyan' => '106', 'bg_white' => '107', ); /** @var array */ private $themes = array(); public function __construct() { $this->isSupported = $this->isSupported(); } /** * @param string|array $style * @param string $text * @return string * @throws InvalidStyleException * @throws \InvalidArgumentException */ public function apply($style, $text) { if (!$this->isStyleForced() && !$this->isSupported()) { return $text; } if (is_string($style)) { $style = array($style); } if (!is_array($style)) { throw new \InvalidArgumentException("Style must be string or array."); } $sequences = array(); foreach ($style as $s) { if (isset($this->themes[$s])) { $sequences = array_merge($sequences, $this->themeSequence($s)); } else if ($this->isValidStyle($s)) { $sequences[] = $this->styleSequence($s); } else { throw new InvalidStyleException($s); } } $sequences = array_filter($sequences, function ($val) { return $val !== null; }); if (empty($sequences)) { return $text; } return $this->escSequence(implode(';', $sequences)) . $text . $this->escSequence(self::RESET_STYLE); } /** * @param bool $forceStyle */ public function setForceStyle($forceStyle) { $this->forceStyle = (bool) $forceStyle; } /** * @return bool */ public function isStyleForced() { return $this->forceStyle; } /** * @param array $themes * @throws InvalidStyleException * @throws \InvalidArgumentException */ public function setThemes(array $themes) { $this->themes = array(); foreach ($themes as $name => $styles) { $this->addTheme($name, $styles); } } /** * @param string $name * @param array|string $styles * @throws \InvalidArgumentException * @throws InvalidStyleException */ public function addTheme($name, $styles) { if (is_string($styles)) { $styles = array($styles); } if (!is_array($styles)) { throw new \InvalidArgumentException("Style must be string or array."); } foreach ($styles as $style) { if (!$this->isValidStyle($style)) { throw new InvalidStyleException($style); } } $this->themes[$name] = $styles; } /** * @return array */ public function getThemes() { return $this->themes; } /** * @param string $name * @return bool */ public function hasTheme($name) { return isset($this->themes[$name]); } /** * @param string $name */ public function removeTheme($name) { unset($this->themes[$name]); } /** * @return bool */ public function isSupported() { if (DIRECTORY_SEPARATOR === '\\') { return getenv('ANSICON') !== false || getenv('ConEmuANSI') === 'ON'; } return function_exists('posix_isatty') && @posix_isatty(STDOUT); } /** * @return bool */ public function are256ColorsSupported() { return DIRECTORY_SEPARATOR === '/' && strpos(getenv('TERM'), '256color') !== false; } /** * @return array */ public function getPossibleStyles() { return array_keys($this->styles); } /** * @param string $name * @return string * @throws InvalidStyleException */ private function themeSequence($name) { $sequences = array(); foreach ($this->themes[$name] as $style) { $sequences[] = $this->styleSequence($style); } return $sequences; } /** * @param string $style * @return string * @throws InvalidStyleException */ private function styleSequence($style) { if (array_key_exists($style, $this->styles)) { return $this->styles[$style]; } if (!$this->are256ColorsSupported()) { return null; } preg_match(self::COLOR256_REGEXP, $style, $matches); $type = $matches[1] === 'bg_' ? self::BACKGROUND : self::FOREGROUND; $value = $matches[2]; return "$type;5;$value"; } /** * @param string $style * @return bool */ private function isValidStyle($style) { return array_key_exists($style, $this->styles) || preg_match(self::COLOR256_REGEXP, $style); } /** * @param string|int $value * @return string */ private function escSequence($value) { return "\033[{$value}m"; } }getCodeSnippet($fileContent, 3);getWholeFile($fileContent);getWholeFileWithLineNumbers($fileContent); 'red', self::TOKEN_COMMENT => 'yellow', self::TOKEN_KEYWORD => 'green', self::TOKEN_DEFAULT => 'default', self::TOKEN_HTML => 'cyan', self::ACTUAL_LINE_MARK => 'red', self::LINE_NUMBER => 'dark_gray', ); /** * @param ConsoleColor $color */ public function __construct(ConsoleColor $color) { $this->color = $color; foreach ($this->defaultTheme as $name => $styles) { if (!$this->color->hasTheme($name)) { $this->color->addTheme($name, $styles); } } } /** * @param string $source * @param int $lineNumber * @param int $linesBefore * @param int $linesAfter * @return string * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException * @throws \InvalidArgumentException */ public function getCodeSnippet($source, $lineNumber, $linesBefore = 2, $linesAfter = 2) { $tokenLines = $this->getHighlightedLines($source); $offset = $lineNumber - $linesBefore - 1; $offset = max($offset, 0); $length = $linesAfter + $linesBefore + 1; $tokenLines = array_slice($tokenLines, $offset, $length, $preserveKeys = true); $lines = $this->colorLines($tokenLines); return $this->lineNumbers($lines, $lineNumber); } /** * @param string $source * @return string * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException * @throws \InvalidArgumentException */ public function getWholeFile($source) { $tokenLines = $this->getHighlightedLines($source); $lines = $this->colorLines($tokenLines); return implode(PHP_EOL, $lines); } /** * @param string $source * @return string * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException * @throws \InvalidArgumentException */ public function getWholeFileWithLineNumbers($source) { $tokenLines = $this->getHighlightedLines($source); $lines = $this->colorLines($tokenLines); return $this->lineNumbers($lines); } /** * @param string $source * @return array */ private function getHighlightedLines($source) { $source = str_replace(array("\r\n", "\r"), "\n", $source); $tokens = $this->tokenize($source); return $this->splitToLines($tokens); } /** * @param string $source * @return array */ private function tokenize($source) { $tokens = token_get_all($source); $output = array(); $currentType = null; $buffer = ''; foreach ($tokens as $token) { if (is_array($token)) { switch ($token[0]) { case T_INLINE_HTML: $newType = self::TOKEN_HTML; break; case T_COMMENT: case T_DOC_COMMENT: $newType = self::TOKEN_COMMENT; break; case T_ENCAPSED_AND_WHITESPACE: case T_CONSTANT_ENCAPSED_STRING: $newType = self::TOKEN_STRING; break; case T_WHITESPACE: break; case T_OPEN_TAG: case T_OPEN_TAG_WITH_ECHO: case T_CLOSE_TAG: case T_STRING: case T_VARIABLE: // Constants case T_DIR: case T_FILE: case T_METHOD_C: case T_DNUMBER: case T_LNUMBER: case T_NS_C: case T_LINE: case T_CLASS_C: case T_FUNC_C: //case T_TRAIT_C: $newType = self::TOKEN_DEFAULT; break; default: // Compatibility with PHP 5.3 if (defined('T_TRAIT_C') && $token[0] === T_TRAIT_C) { $newType = self::TOKEN_DEFAULT; } else { $newType = self::TOKEN_KEYWORD; } } } else { $newType = $token === '"' ? self::TOKEN_STRING : self::TOKEN_KEYWORD; } if ($currentType === null) { $currentType = $newType; } if ($currentType != $newType) { $output[] = array($currentType, $buffer); $buffer = ''; $currentType = $newType; } $buffer .= is_array($token) ? $token[1] : $token; } if (isset($newType)) { $output[] = array($newType, $buffer); } return $output; } /** * @param array $tokens * @return array */ private function splitToLines(array $tokens) { $lines = array(); $line = array(); foreach ($tokens as $token) { foreach (explode("\n", $token[1]) as $count => $tokenLine) { if ($count > 0) { $lines[] = $line; $line = array(); } if ($tokenLine === '') { continue; } $line[] = array($token[0], $tokenLine); } } $lines[] = $line; return $lines; } /** * @param array $tokenLines * @return array * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException * @throws \InvalidArgumentException */ private function colorLines(array $tokenLines) { $lines = array(); foreach ($tokenLines as $lineCount => $tokenLine) { $line = ''; foreach ($tokenLine as $token) { list($tokenType, $tokenValue) = $token; if ($this->color->hasTheme($tokenType)) { $line .= $this->color->apply($tokenType, $tokenValue); } else { $line .= $tokenValue; } } $lines[$lineCount] = $line; } return $lines; } /** * @param array $lines * @param null|int $markLine * @return string * @throws \JakubOnderka\PhpConsoleColor\InvalidStyleException */ private function lineNumbers(array $lines, $markLine = null) { end($lines); $lineStrlen = strlen(key($lines) + 1); $snippet = ''; foreach ($lines as $i => $line) { if ($markLine !== null) { $snippet .= ($markLine === $i + 1 ? $this->color->apply(self::ACTUAL_LINE_MARK, ' > ') : ' '); } $snippet .= $this->color->apply(self::LINE_NUMBER, str_pad($i + 1, $lineStrlen, ' ', STR_PAD_LEFT) . '| '); $snippet .= $line . PHP_EOL; } return $snippet; } }\'[^\\\\\']*+(?:\\\\.[^\\\\\']*+)*+\') (?"[^\\\\"]*+(?:\\\\.[^\\\\"]*+)*+") (?(?&singleQuotedString)|(?&doubleQuotedString)) (?/\*[^*]*+(?:\*(?!/)[^*]*+)*+\*/) (?\{[^\'"/{}]*+(?:(?:(?&string)|(?&comment)|(?&code)|/)[^\'"/{}]*+)*+}) )'; const RULE_BLOCK = '(?[a-z_]++):(?[^\'"/{};]*+(?:(?:(?&string)|(?&comment)|(?&code)|/|})[^\'"/{};]*+)*+);'; $usedTerminals = array_flip(array( 'T_VARIABLE', 'T_STRING', 'T_INLINE_HTML', 'T_ENCAPSED_AND_WHITESPACE', 'T_LNUMBER', 'T_DNUMBER', 'T_CONSTANT_ENCAPSED_STRING', 'T_STRING_VARNAME', 'T_NUM_STRING' )); $unusedNonterminals = array_flip(array( 'case_separator', 'optional_comma' )); function regex($regex) { return '~' . LIB . '(?:' . str_replace('~', '\~', $regex) . ')~'; } function magicSplit($regex, $string) { $pieces = preg_split(regex('(?:(?&string)|(?&comment)|(?&code))(*SKIP)(*FAIL)|' . $regex), $string); foreach ($pieces as &$piece) { $piece = trim($piece); } return array_filter($pieces); } echo '
';

////////////////////
////////////////////
////////////////////

list($defs, $ruleBlocks) = magicSplit('%%', file_get_contents(GRAMMAR_FILE));

if ('' !== trim(preg_replace(regex(RULE_BLOCK), '', $ruleBlocks))) {
    die('Not all rule blocks were properly recognized!');
}

preg_match_all(regex(RULE_BLOCK), $ruleBlocks, $ruleBlocksMatches, PREG_SET_ORDER);
foreach ($ruleBlocksMatches as $match) {
    $ruleBlockName = $match['name'];
    $rules = magicSplit('\|', $match['rules']);

    foreach ($rules as &$rule) {
        $parts = magicSplit('\s+', $rule);
        $usedParts = array();

        foreach ($parts as $part) {
            if ('{' === $part[0]) {
                preg_match_all('~\$([0-9]+)~', $part, $backReferencesMatches, PREG_SET_ORDER);
                foreach ($backReferencesMatches as $match) {
                    $usedParts[$match[1]] = true;
                }
            }
        }

        $i = 1;
        foreach ($parts as &$part) {
            if ('/' === $part[0]) {
                continue;
            }

            if (isset($usedParts[$i])) {
                if ('\'' === $part[0] || '{' === $part[0]
                    || (ctype_upper($part[0]) && !isset($usedTerminals[$part]))
                    || (ctype_lower($part[0]) && isset($unusedNonterminals[$part]))
                ) {
                    $part = '' . $part . '';
                } else {
                    $part = '' . $part . '';
                }
            } elseif ((ctype_upper($part[0]) && isset($usedTerminals[$part]))
                      || (ctype_lower($part[0]) && !isset($unusedNonterminals[$part]))

            ) {
                $part = '' . $part . '';
            }

            ++$i;
        }

        $rule = implode(' ', $parts);
    }

    echo $ruleBlockName, ':', "\n", '      ', implode("\n" . '    | ', $rules), "\n", ';', "\n\n";
}
 'Php5',
    __DIR__ . '/php7.y' => 'Php7',
];

$tokensFile     = __DIR__ . '/tokens.y';
$tokensTemplate = __DIR__ . '/tokens.template';
$skeletonFile   = __DIR__ . '/parser.template';
$tmpGrammarFile = __DIR__ . '/tmp_parser.phpy';
$tmpResultFile  = __DIR__ . '/tmp_parser.php';
$resultDir = __DIR__ . '/../lib/PhpParser/Parser';
$tokensResultsFile = $resultDir . '/Tokens.php';

// check for kmyacc.exe binary in this directory, otherwise fall back to global name
$kmyacc = __DIR__ . '/kmyacc.exe';
if (!file_exists($kmyacc)) {
    $kmyacc = 'kmyacc';
}

$options = array_flip($argv);
$optionDebug = isset($options['--debug']);
$optionKeepTmpGrammar = isset($options['--keep-tmp-grammar']);

///////////////////////////////
/// Utility regex constants ///
///////////////////////////////

const LIB = '(?(DEFINE)
    (?\'[^\\\\\']*+(?:\\\\.[^\\\\\']*+)*+\')
    (?"[^\\\\"]*+(?:\\\\.[^\\\\"]*+)*+")
    (?(?&singleQuotedString)|(?&doubleQuotedString))
    (?/\*[^*]*+(?:\*(?!/)[^*]*+)*+\*/)
    (?\{[^\'"/{}]*+(?:(?:(?&string)|(?&comment)|(?&code)|/)[^\'"/{}]*+)*+})
)';

const PARAMS = '\[(?[^[\]]*+(?:\[(?¶ms)\][^[\]]*+)*+)\]';
const ARGS   = '\((?[^()]*+(?:\((?&args)\)[^()]*+)*+)\)';

///////////////////
/// Main script ///
///////////////////

$tokens = file_get_contents($tokensFile);

foreach ($grammarFileToName as $grammarFile => $name) {
    echo "Building temporary $name grammar file.\n";

    $grammarCode = file_get_contents($grammarFile);
    $grammarCode = str_replace('%tokens', $tokens, $grammarCode);

    $grammarCode = resolveNodes($grammarCode);
    $grammarCode = resolveMacros($grammarCode);
    $grammarCode = resolveStackAccess($grammarCode);

    file_put_contents($tmpGrammarFile, $grammarCode);

    $additionalArgs = $optionDebug ? '-t -v' : '';

    echo "Building $name parser.\n";
    $output = trim(shell_exec("$kmyacc $additionalArgs -l -m $skeletonFile -p $name $tmpGrammarFile 2>&1"));
    echo "Output: \"$output\"\n";

    $resultCode = file_get_contents($tmpResultFile);
    $resultCode = removeTrailingWhitespace($resultCode);

    ensureDirExists($resultDir);
    file_put_contents("$resultDir/$name.php", $resultCode);
    unlink($tmpResultFile);

    echo "Building token definition.\n";
    $output = trim(shell_exec("$kmyacc -l -m $tokensTemplate $tmpGrammarFile 2>&1"));
    assert($output === '');
    rename($tmpResultFile, $tokensResultsFile);

    if (!$optionKeepTmpGrammar) {
        unlink($tmpGrammarFile);
    }
}

///////////////////////////////
/// Preprocessing functions ///
///////////////////////////////

function resolveNodes($code) {
    return preg_replace_callback(
        '~\b(?[A-Z][a-zA-Z_\\\\]++)\s*' . PARAMS . '~',
        function($matches) {
            // recurse
            $matches['params'] = resolveNodes($matches['params']);

            $params = magicSplit(
                '(?:' . PARAMS . '|' . ARGS . ')(*SKIP)(*FAIL)|,',
                $matches['params']
            );

            $paramCode = '';
            foreach ($params as $param) {
                $paramCode .= $param . ', ';
            }

            return 'new ' . $matches['name'] . '(' . $paramCode . 'attributes())';
        },
        $code
    );
}

function resolveMacros($code) {
    return preg_replace_callback(
        '~\b(?)(?!array\()(?[a-z][A-Za-z]++)' . ARGS . '~',
        function($matches) {
            // recurse
            $matches['args'] = resolveMacros($matches['args']);

            $name = $matches['name'];
            $args = magicSplit(
                '(?:' . PARAMS . '|' . ARGS . ')(*SKIP)(*FAIL)|,',
                $matches['args']
            );

            if ('attributes' == $name) {
                assertArgs(0, $args, $name);
                return '$this->startAttributeStack[#1] + $this->endAttributes';
            }

            if ('init' == $name) {
                return '$$ = array(' . implode(', ', $args) . ')';
            }

            if ('push' == $name) {
                assertArgs(2, $args, $name);

                return $args[0] . '[] = ' . $args[1] . '; $$ = ' . $args[0];
            }

            if ('pushNormalizing' == $name) {
                assertArgs(2, $args, $name);

                return 'if (is_array(' . $args[1] . ')) { $$ = array_merge(' . $args[0] . ', ' . $args[1] . '); }'
                     . ' else { ' . $args[0] . '[] = ' . $args[1] . '; $$ = ' . $args[0] . '; }';
            }

            if ('toArray' == $name) {
                assertArgs(1, $args, $name);

                return 'is_array(' . $args[0] . ') ? ' . $args[0] . ' : array(' . $args[0] . ')';
            }

            if ('parseVar' == $name) {
                assertArgs(1, $args, $name);

                return 'substr(' . $args[0] . ', 1)';
            }

            if ('parseEncapsed' == $name) {
                assertArgs(3, $args, $name);

                return 'foreach (' . $args[0] . ' as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) {'
                     . ' $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, ' . $args[1] . ', ' . $args[2] . '); } }';
            }

            if ('parseEncapsedDoc' == $name) {
                assertArgs(2, $args, $name);

                return 'foreach (' . $args[0] . ' as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) {'
                     . ' $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, null, ' . $args[1] . '); } }'
                     . ' $s->value = preg_replace(\'~(\r\n|\n|\r)\z~\', \'\', $s->value);'
                     . ' if (\'\' === $s->value) array_pop(' . $args[0] . ');';
            }

            if ('makeNop' == $name) {
                assertArgs(2, $args, $name);

                return '$startAttributes = ' . $args[1] . ';'
                . ' if (isset($startAttributes[\'comments\']))'
                . ' { ' . $args[0] . ' = new Stmt\Nop([\'comments\' => $startAttributes[\'comments\']]); }'
                . ' else { ' . $args[0] . ' = null; }';
            }

            if ('strKind' == $name) {
                assertArgs(1, $args, $name);

                return '(' . $args[0] . '[0] === "\'" || (' . $args[0] . '[1] === "\'" && '
                     . '(' . $args[0] . '[0] === \'b\' || ' . $args[0] . '[0] === \'B\')) '
                     . '? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED)';
            }

            if ('setDocStringAttrs' == $name) {
                assertArgs(2, $args, $name);

                return $args[0] . '[\'kind\'] = strpos(' . $args[1] . ', "\'") === false '
                     . '? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; '
                     . 'preg_match(\'/\A[bB]?<<<[ \t]*[\\\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\\\'"]?(?:\r\n|\n|\r)\z/\', ' . $args[1] . ', $matches); '
                     . $args[0] . '[\'docLabel\'] = $matches[1];';
            }

            if ('prependLeadingComments' == $name) {
                assertArgs(1, $args, $name);

                return '$attrs = $this->startAttributeStack[#1]; $stmts = ' . $args[0] . '; '
                . 'if (!empty($attrs[\'comments\']) && isset($stmts[0])) {'
                . '$stmts[0]->setAttribute(\'comments\', '
                . 'array_merge($attrs[\'comments\'], $stmts[0]->getAttribute(\'comments\', []))); }';
            }

            return $matches[0];
        },
        $code
    );
}

function assertArgs($num, $args, $name) {
    if ($num != count($args)) {
        die('Wrong argument count for ' . $name . '().');
    }
}

function resolveStackAccess($code) {
    $code = preg_replace('/\$\d+/', '$this->semStack[$0]', $code);
    $code = preg_replace('/#(\d+)/', '$$1', $code);
    return $code;
}

function removeTrailingWhitespace($code) {
    $lines = explode("\n", $code);
    $lines = array_map('rtrim', $lines);
    return implode("\n", $lines);
}

function ensureDirExists($dir) {
    if (!is_dir($dir)) {
        mkdir($dir, 0777, true);
    }
}

//////////////////////////////
/// Regex helper functions ///
//////////////////////////////

function regex($regex) {
    return '~' . LIB . '(?:' . str_replace('~', '\~', $regex) . ')~';
}

function magicSplit($regex, $string) {
    $pieces = preg_split(regex('(?:(?&string)|(?&comment)|(?&code))(*SKIP)(*FAIL)|' . $regex), $string);

    foreach ($pieces as &$piece) {
        $piece = trim($piece);
    }

    if ($pieces === ['']) {
        return [];
    }

    return $pieces;
}
name = $name;
    }

    /**
     * Extends a class.
     *
     * @param Name|string $class Name of class to extend
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function extend($class) {
        $this->extends = $this->normalizeName($class);

        return $this;
    }

    /**
     * Implements one or more interfaces.
     *
     * @param Name|string ...$interfaces Names of interfaces to implement
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function implement() {
        foreach (func_get_args() as $interface) {
            $this->implements[] = $this->normalizeName($interface);
        }

        return $this;
    }

    /**
     * Makes the class abstract.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makeAbstract() {
        $this->setModifier(Stmt\Class_::MODIFIER_ABSTRACT);

        return $this;
    }

    /**
     * Makes the class final.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makeFinal() {
        $this->setModifier(Stmt\Class_::MODIFIER_FINAL);

        return $this;
    }

    /**
     * Adds a statement.
     *
     * @param Stmt|PhpParser\Builder $stmt The statement to add
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function addStmt($stmt) {
        $stmt = $this->normalizeNode($stmt);

        $targets = array(
            'Stmt_TraitUse'    => &$this->uses,
            'Stmt_ClassConst'  => &$this->constants,
            'Stmt_Property'    => &$this->properties,
            'Stmt_ClassMethod' => &$this->methods,
        );

        $type = $stmt->getType();
        if (!isset($targets[$type])) {
            throw new \LogicException(sprintf('Unexpected node of type "%s"', $type));
        }

        $targets[$type][] = $stmt;

        return $this;
    }

    /**
     * Returns the built class node.
     *
     * @return Stmt\Class_ The built class node
     */
    public function getNode() {
        return new Stmt\Class_($this->name, array(
            'type' => $this->type,
            'extends' => $this->extends,
            'implements' => $this->implements,
            'stmts' => array_merge($this->uses, $this->constants, $this->properties, $this->methods),
        ), $this->attributes);
    }
}addStmt($stmt);
        }

        return $this;
    }

    /**
     * Sets doc comment for the declaration.
     *
     * @param PhpParser\Comment\Doc|string $docComment Doc comment to set
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function setDocComment($docComment) {
        $this->attributes['comments'] = array(
            $this->normalizeDocComment($docComment)
        );

        return $this;
    }
}name = $name;
    }

    /**
     * Adds a statement.
     *
     * @param Node|PhpParser\Builder $stmt The statement to add
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function addStmt($stmt) {
        $this->stmts[] = $this->normalizeNode($stmt);

        return $this;
    }

    /**
     * Returns the built function node.
     *
     * @return Stmt\Function_ The built function node
     */
    public function getNode() {
        return new Stmt\Function_($this->name, array(
            'byRef'      => $this->returnByRef,
            'params'     => $this->params,
            'returnType' => $this->returnType,
            'stmts'      => $this->stmts,
        ), $this->attributes);
    }
}
returnByRef = true;

        return $this;
    }

    /**
     * Adds a parameter.
     *
     * @param Node\Param|Param $param The parameter to add
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function addParam($param) {
        $param = $this->normalizeNode($param);

        if (!$param instanceof Node\Param) {
            throw new \LogicException(sprintf('Expected parameter node, got "%s"', $param->getType()));
        }

        $this->params[] = $param;

        return $this;
    }

    /**
     * Adds multiple parameters.
     *
     * @param array $params The parameters to add
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function addParams(array $params) {
        foreach ($params as $param) {
            $this->addParam($param);
        }

        return $this;
    }

    /**
     * Sets the return type for PHP 7.
     *
     * @param string|Node\Name $type One of array, callable, string, int, float, bool,
     *                               or a class/interface name.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function setReturnType($type)
    {
        if (in_array($type, array('array', 'callable', 'string', 'int', 'float', 'bool'))) {
            $this->returnType = $type;
        } else {
            $this->returnType = $this->normalizeName($type);
        }

        return $this;
    }
}
name = $name;
    }

    /**
     * Extends one or more interfaces.
     *
     * @param Name|string ...$interfaces Names of interfaces to extend
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function extend() {
        foreach (func_get_args() as $interface) {
            $this->extends[] = $this->normalizeName($interface);
        }

        return $this;
    }

    /**
     * Adds a statement.
     *
     * @param Stmt|PhpParser\Builder $stmt The statement to add
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function addStmt($stmt) {
        $stmt = $this->normalizeNode($stmt);

        $type = $stmt->getType();
        switch ($type) {
            case 'Stmt_ClassConst':
                $this->constants[] = $stmt;
                break;

            case 'Stmt_ClassMethod':
                // we erase all statements in the body of an interface method
                $stmt->stmts = null;
                $this->methods[] = $stmt;
                break;

            default:
                throw new \LogicException(sprintf('Unexpected node of type "%s"', $type));
        }

        return $this;
    }

    /**
     * Returns the built interface node.
     *
     * @return Stmt\Interface_ The built interface node
     */
    public function getNode() {
        return new Stmt\Interface_($this->name, array(
            'extends' => $this->extends,
            'stmts' => array_merge($this->constants, $this->methods),
        ), $this->attributes);
    }
}name = $name;
    }

    /**
     * Makes the method public.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makePublic() {
        $this->setModifier(Stmt\Class_::MODIFIER_PUBLIC);

        return $this;
    }

    /**
     * Makes the method protected.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makeProtected() {
        $this->setModifier(Stmt\Class_::MODIFIER_PROTECTED);

        return $this;
    }

    /**
     * Makes the method private.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makePrivate() {
        $this->setModifier(Stmt\Class_::MODIFIER_PRIVATE);

        return $this;
    }

    /**
     * Makes the method static.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makeStatic() {
        $this->setModifier(Stmt\Class_::MODIFIER_STATIC);

        return $this;
    }

    /**
     * Makes the method abstract.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makeAbstract() {
        if (!empty($this->stmts)) {
            throw new \LogicException('Cannot make method with statements abstract');
        }

        $this->setModifier(Stmt\Class_::MODIFIER_ABSTRACT);
        $this->stmts = null; // abstract methods don't have statements

        return $this;
    }

    /**
     * Makes the method final.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makeFinal() {
        $this->setModifier(Stmt\Class_::MODIFIER_FINAL);

        return $this;
    }

    /**
     * Adds a statement.
     *
     * @param Node|PhpParser\Builder $stmt The statement to add
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function addStmt($stmt) {
        if (null === $this->stmts) {
            throw new \LogicException('Cannot add statements to an abstract method');
        }

        $this->stmts[] = $this->normalizeNode($stmt);

        return $this;
    }

    /**
     * Returns the built method node.
     *
     * @return Stmt\ClassMethod The built method node
     */
    public function getNode() {
        return new Stmt\ClassMethod($this->name, array(
            'type'       => $this->type,
            'byRef'      => $this->returnByRef,
            'params'     => $this->params,
            'returnType' => $this->returnType,
            'stmts'      => $this->stmts,
        ), $this->attributes);
    }
}
name = null !== $name ? $this->normalizeName($name) : null;
    }

    /**
     * Adds a statement.
     *
     * @param Node|PhpParser\Builder $stmt The statement to add
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function addStmt($stmt) {
        $this->stmts[] = $this->normalizeNode($stmt);

        return $this;
    }

    /**
     * Adds multiple statements.
     *
     * @param array $stmts The statements to add
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function addStmts(array $stmts) {
        foreach ($stmts as $stmt) {
            $this->addStmt($stmt);
        }

        return $this;
    }

    /**
     * Returns the built node.
     *
     * @return Node The built node
     */
    public function getNode() {
        return new Stmt\Namespace_($this->name, $this->stmts);
    }
}
name = $name;
    }

    /**
     * Sets default value for the parameter.
     *
     * @param mixed $value Default value to use
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function setDefault($value) {
        $this->default = $this->normalizeValue($value);

        return $this;
    }

    /**
     * Sets type hint for the parameter.
     *
     * @param string|Node\Name $type Type hint to use
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function setTypeHint($type) {
        if (in_array($type, array('array', 'callable', 'string', 'int', 'float', 'bool'))) {
            $this->type = $type;
        } else {
            $this->type = $this->normalizeName($type);
        }

        return $this;
    }

    /**
     * Make the parameter accept the value by reference.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makeByRef() {
        $this->byRef = true;

        return $this;
    }

    /**
     * Returns the built parameter node.
     *
     * @return Node\Param The built parameter node
     */
    public function getNode() {
        return new Node\Param(
            $this->name, $this->default, $this->type, $this->byRef
        );
    }
}
name = $name;
    }

    /**
     * Makes the property public.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makePublic() {
        $this->setModifier(Stmt\Class_::MODIFIER_PUBLIC);

        return $this;
    }

    /**
     * Makes the property protected.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makeProtected() {
        $this->setModifier(Stmt\Class_::MODIFIER_PROTECTED);

        return $this;
    }

    /**
     * Makes the property private.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makePrivate() {
        $this->setModifier(Stmt\Class_::MODIFIER_PRIVATE);

        return $this;
    }

    /**
     * Makes the property static.
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function makeStatic() {
        $this->setModifier(Stmt\Class_::MODIFIER_STATIC);

        return $this;
    }

    /**
     * Sets default value for the property.
     *
     * @param mixed $value Default value to use
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function setDefault($value) {
        $this->default = $this->normalizeValue($value);

        return $this;
    }

    /**
     * Sets doc comment for the property.
     *
     * @param PhpParser\Comment\Doc|string $docComment Doc comment to set
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function setDocComment($docComment) {
        $this->attributes = array(
            'comments' => array($this->normalizeDocComment($docComment))
        );

        return $this;
    }

    /**
     * Returns the built class node.
     *
     * @return Stmt\Property The built property node
     */
    public function getNode() {
        return new Stmt\Property(
            $this->type !== 0 ? $this->type : Stmt\Class_::MODIFIER_PUBLIC,
            array(
                new Stmt\PropertyProperty($this->name, $this->default)
            ),
            $this->attributes
        );
    }
}name = $name;
    }

    /**
     * Adds a statement.
     *
     * @param Stmt|PhpParser\Builder $stmt The statement to add
     *
     * @return $this The builder instance (for fluid interface)
     */
    public function addStmt($stmt) {
        $stmt = $this->normalizeNode($stmt);

        if ($stmt instanceof Stmt\Property) {
            $this->properties[] = $stmt;
        } else if ($stmt instanceof Stmt\ClassMethod) {
            $this->methods[] = $stmt;
        } else {
            throw new \LogicException(sprintf('Unexpected node of type "%s"', $stmt->getType()));
        }

        return $this;
    }

    /**
     * Returns the built trait node.
     *
     * @return Stmt\Trait_ The built interface node
     */
    public function getNode() {
        return new Stmt\Trait_(
            $this->name, array_merge($this->properties, $this->methods), $this->attributes
        );
    }
}
name = $this->normalizeName($name);
        $this->type = $type;
    }

    /**
     * Sets alias for used name.
     *
     * @param string $alias Alias to use (last component of full name by default)
     *
     * @return $this The builder instance (for fluid interface)
     */
    protected function as_($alias) {
        $this->alias = $alias;
        return $this;
    }
    public function __call($name, $args) {
        if (method_exists($this, $name . '_')) {
            return call_user_func_array(array($this, $name . '_'), $args);
        }

        throw new \LogicException(sprintf('Method "%s" does not exist', $name));
    }

    /**
     * Returns the built node.
     *
     * @return Node The built node
     */
    public function getNode() {
        $alias = null !== $this->alias ? $this->alias : $this->name->getLast();
        return new Stmt\Use_(array(
            new Stmt\UseUse($this->name, $alias)
        ), $this->type);
    }
}
getNode();
        } elseif ($node instanceof Node) {
            return $node;
        }

        throw new \LogicException('Expected node or builder object');
    }

    /**
     * Normalizes a name: Converts plain string names to PhpParser\Node\Name.
     *
     * @param Name|string $name The name to normalize
     *
     * @return Name The normalized name
     */
    protected function normalizeName($name) {
        if ($name instanceof Name) {
            return $name;
        } elseif (is_string($name)) {
            if (!$name) {
                throw new \LogicException('Name cannot be empty');
            }

            if ($name[0] == '\\') {
                return new Name\FullyQualified(substr($name, 1));
            } elseif (0 === strpos($name, 'namespace\\')) {
                return new Name\Relative(substr($name, strlen('namespace\\')));
            } else {
                return new Name($name);
            }
        }

        throw new \LogicException('Name must be a string or an instance of PhpParser\Node\Name');
    }

    /**
     * Normalizes a value: Converts nulls, booleans, integers,
     * floats, strings and arrays into their respective nodes
     *
     * @param mixed $value The value to normalize
     *
     * @return Expr The normalized value
     */
    protected function normalizeValue($value) {
        if ($value instanceof Node) {
            return $value;
        } elseif (is_null($value)) {
            return new Expr\ConstFetch(
                new Name('null')
            );
        } elseif (is_bool($value)) {
            return new Expr\ConstFetch(
                new Name($value ? 'true' : 'false')
            );
        } elseif (is_int($value)) {
            return new Scalar\LNumber($value);
        } elseif (is_float($value)) {
            return new Scalar\DNumber($value);
        } elseif (is_string($value)) {
            return new Scalar\String_($value);
        } elseif (is_array($value)) {
            $items = array();
            $lastKey = -1;
            foreach ($value as $itemKey => $itemValue) {
                // for consecutive, numeric keys don't generate keys
                if (null !== $lastKey && ++$lastKey === $itemKey) {
                    $items[] = new Expr\ArrayItem(
                        $this->normalizeValue($itemValue)
                    );
                } else {
                    $lastKey = null;
                    $items[] = new Expr\ArrayItem(
                        $this->normalizeValue($itemValue),
                        $this->normalizeValue($itemKey)
                    );
                }
            }

            return new Expr\Array_($items);
        } else {
            throw new \LogicException('Invalid value');
        }
    }

    /**
     * Normalizes a doc comment: Converts plain strings to PhpParser\Comment\Doc.
     *
     * @param Comment\Doc|string $docComment The doc comment to normalize
     *
     * @return Comment\Doc The normalized doc comment
     */
    protected function normalizeDocComment($docComment) {
        if ($docComment instanceof Comment\Doc) {
            return $docComment;
        } else if (is_string($docComment)) {
            return new Comment\Doc($docComment);
        } else {
            throw new \LogicException('Doc comment must be a string or an instance of PhpParser\Comment\Doc');
        }
    }

    /**
     * Sets a modifier in the $this->type property.
     *
     * @param int $modifier Modifier to set
     */
    protected function setModifier($modifier) {
        Stmt\Class_::verifyModifier($this->type, $modifier);
        $this->type |= $modifier;
    }
}
text = $text;
        $this->line = $startLine;
        $this->filePos = $startFilePos;
    }

    /**
     * Gets the comment text.
     *
     * @return string The comment text (including comment delimiters like /*)
     */
    public function getText() {
        return $this->text;
    }

    /**
     * Sets the comment text.
     *
     * @param string $text The comment text (including comment delimiters like /*)
     *
     * @deprecated Construct a new comment instead
     */
    public function setText($text) {
        $this->text = $text;
    }

    /**
     * Gets the line number the comment started on.
     *
     * @return int Line number
     */
    public function getLine() {
        return $this->line;
    }

    /**
     * Sets the line number the comment started on.
     *
     * @param int $line Line number
     *
     * @deprecated Construct a new comment instead
     */
    public function setLine($line) {
        $this->line = $line;
    }

    /**
     * Gets the file offset the comment started on.
     *
     * @return int File offset
     */
    public function getFilePos() {
        return $this->filePos;
    }

    /**
     * Gets the comment text.
     *
     * @return string The comment text (including comment delimiters like /*)
     */
    public function __toString() {
        return $this->text;
    }

    /**
     * Gets the reformatted comment text.
     *
     * "Reformatted" here means that we try to clean up the whitespace at the
     * starts of the lines. This is necessary because we receive the comments
     * without trailing whitespace on the first line, but with trailing whitespace
     * on all subsequent lines.
     *
     * @return mixed|string
     */
    public function getReformattedText() {
        $text = trim($this->text);
        $newlinePos = strpos($text, "\n");
        if (false === $newlinePos) {
            // Single line comments don't need further processing
            return $text;
        } elseif (preg_match('((*BSR_ANYCRLF)(*ANYCRLF)^.*(?:\R\s+\*.*)+$)', $text)) {
            // Multi line comment of the type
            //
            //     /*
            //      * Some text.
            //      * Some more text.
            //      */
            //
            // is handled by replacing the whitespace sequences before the * by a single space
            return preg_replace('(^\s+\*)m', ' *', $this->text);
        } elseif (preg_match('(^/\*\*?\s*[\r\n])', $text) && preg_match('(\n(\s*)\*/$)', $text, $matches)) {
            // Multi line comment of the type
            //
            //    /*
            //        Some text.
            //        Some more text.
            //    */
            //
            // is handled by removing the whitespace sequence on the line before the closing
            // */ on all lines. So if the last line is "    */", then "    " is removed at the
            // start of all lines.
            return preg_replace('(^' . preg_quote($matches[1]) . ')m', '', $text);
        } elseif (preg_match('(^/\*\*?\s*(?!\s))', $text, $matches)) {
            // Multi line comment of the type
            //
            //     /* Some text.
            //        Some more text.
            //          Indented text.
            //        Even more text. */
            //
            // is handled by removing the difference between the shortest whitespace prefix on all
            // lines and the length of the "/* " opening sequence.
            $prefixLen = $this->getShortestWhitespacePrefixLen(substr($text, $newlinePos + 1));
            $removeLen = $prefixLen - strlen($matches[0]);
            return preg_replace('(^\s{' . $removeLen . '})m', '', $text);
        }

        // No idea how to format this comment, so simply return as is
        return $text;
    }

    private function getShortestWhitespacePrefixLen($str) {
        $lines = explode("\n", $str);
        $shortestPrefixLen = INF;
        foreach ($lines as $line) {
            preg_match('(^\s*)', $line, $matches);
            $prefixLen = strlen($matches[0]);
            if ($prefixLen < $shortestPrefixLen) {
                $shortestPrefixLen = $prefixLen;
            }
        }
        return $shortestPrefixLen;
    }
}rawMessage = (string) $message;
        if (is_array($attributes)) {
            $this->attributes = $attributes;
        } else {
            $this->attributes = array('startLine' => $attributes);
        }
        $this->updateMessage();
    }

    /**
     * Gets the error message
     *
     * @return string Error message
     */
    public function getRawMessage() {
        return $this->rawMessage;
    }

    /**
     * Gets the line the error starts in.
     *
     * @return int Error start line
     */
    public function getStartLine() {
        return isset($this->attributes['startLine']) ? $this->attributes['startLine'] : -1;
    }

    /**
     * Gets the line the error ends in.
     *
     * @return int Error end line
     */
    public function getEndLine() {
        return isset($this->attributes['endLine']) ? $this->attributes['endLine'] : -1;
    }


    /**
     * Gets the attributes of the node/token the error occurred at.
     *
     * @return array
     */
    public function getAttributes() {
        return $this->attributes;
    }

    /**
     * Sets the line of the PHP file the error occurred in.
     *
     * @param string $message Error message
     */
    public function setRawMessage($message) {
        $this->rawMessage = (string) $message;
        $this->updateMessage();
    }

    /**
     * Sets the line the error starts in.
     *
     * @param int $line Error start line
     */
    public function setStartLine($line) {
        $this->attributes['startLine'] = (int) $line;
        $this->updateMessage();
    }

    /**
     * Returns whether the error has start and end column information.
     *
     * For column information enable the startFilePos and endFilePos in the lexer options.
     *
     * @return bool
     */
    public function hasColumnInfo() {
        return isset($this->attributes['startFilePos']) && isset($this->attributes['endFilePos']);
    }

    /**
     * Gets the start column (1-based) into the line where the error started.
     *
     * @param string $code Source code of the file
     * @return int
     */
    public function getStartColumn($code) {
        if (!$this->hasColumnInfo()) {
            throw new \RuntimeException('Error does not have column information');
        }

        return $this->toColumn($code, $this->attributes['startFilePos']);
    }

    /**
     * Gets the end column (1-based) into the line where the error ended.
     *
     * @param string $code Source code of the file
     * @return int
     */
    public function getEndColumn($code) {
        if (!$this->hasColumnInfo()) {
            throw new \RuntimeException('Error does not have column information');
        }

        return $this->toColumn($code, $this->attributes['endFilePos']);
    }

    private function toColumn($code, $pos) {
        if ($pos > strlen($code)) {
            throw new \RuntimeException('Invalid position information');
        }

        $lineStartPos = strrpos($code, "\n", $pos - strlen($code));
        if (false === $lineStartPos) {
            $lineStartPos = -1;
        }

        return $pos - $lineStartPos;
    }

    /**
     * Updates the exception message after a change to rawMessage or rawLine.
     */
    protected function updateMessage() {
        $this->message = $this->rawMessage;

        if (-1 === $this->getStartLine()) {
            $this->message .= ' on unknown line';
        } else {
            $this->message .= ' on line ' . $this->getStartLine();
        }
    }

    /** @deprecated Use getStartLine() instead */
    public function getRawLine() {
        return $this->getStartLine();
    }

    /** @deprecated Use setStartLine() instead */
    public function setRawLine($line) {
        $this->setStartLine($line);
    }
}
 array(
                'finally'       => Tokens::T_FINALLY,
                'yield'         => Tokens::T_YIELD,
            ),
        );

        $this->newKeywords = array();
        foreach ($newKeywordsPerVersion as $version => $newKeywords) {
            if (version_compare(PHP_VERSION, $version, '>=')) {
                break;
            }

            $this->newKeywords += $newKeywords;
        }

        if (version_compare(PHP_VERSION, self::PHP_7_0, '>=')) {
            return;
        }
        $this->tokenMap[self::T_COALESCE]   = Tokens::T_COALESCE;
        $this->tokenMap[self::T_SPACESHIP]  = Tokens::T_SPACESHIP;
        $this->tokenMap[self::T_YIELD_FROM] = Tokens::T_YIELD_FROM;

        if (version_compare(PHP_VERSION, self::PHP_5_6, '>=')) {
            return;
        }
        $this->tokenMap[self::T_ELLIPSIS]  = Tokens::T_ELLIPSIS;
        $this->tokenMap[self::T_POW]       = Tokens::T_POW;
        $this->tokenMap[self::T_POW_EQUAL] = Tokens::T_POW_EQUAL;
    }

    public function startLexing($code) {
        $this->inObjectAccess = false;

        $preprocessedCode = $this->preprocessCode($code);
        parent::startLexing($preprocessedCode);
        if ($preprocessedCode !== $code) {
            $this->postprocessTokens();
        }

        // Set code property back to the original code, so __halt_compiler()
        // handling and (start|end)FilePos attributes use the correct offsets
        $this->code = $code;
    }

    /*
     * Replaces new features in the code by ~__EMU__{NAME}__{DATA}__~ sequences.
     * ~LABEL~ is never valid PHP code, that's why we can (to some degree) safely
     * use it here.
     * Later when preprocessing the tokens these sequences will either be replaced
     * by real tokens or replaced with their original content (e.g. if they occurred
     * inside a string, i.e. a place where they don't have a special meaning).
     */
    protected function preprocessCode($code) {
        if (version_compare(PHP_VERSION, self::PHP_7_0, '>=')) {
            return $code;
        }

        $code = str_replace('??', '~__EMU__COALESCE__~', $code);
        $code = str_replace('<=>', '~__EMU__SPACESHIP__~', $code);
        $code = preg_replace_callback('(yield[ \n\r\t]+from)', function($matches) {
            // Encoding $0 in order to preserve exact whitespace
            return '~__EMU__YIELDFROM__' . bin2hex($matches[0]) . '__~';
        }, $code);

        if (version_compare(PHP_VERSION, self::PHP_5_6, '>=')) {
            return $code;
        }

        $code = str_replace('...', '~__EMU__ELLIPSIS__~', $code);
        $code = preg_replace('((?tokens); $i < $c; ++$i) {
            // first check that the following tokens are of form ~LABEL~,
            // then match the __EMU__... sequence.
            if ('~' === $this->tokens[$i]
                && isset($this->tokens[$i + 2])
                && '~' === $this->tokens[$i + 2]
                && T_STRING === $this->tokens[$i + 1][0]
                && preg_match('(^__EMU__([A-Z]++)__(?:([A-Za-z0-9]++)__)?$)', $this->tokens[$i + 1][1], $matches)
            ) {
                if ('ELLIPSIS' === $matches[1]) {
                    $replace = array(
                        array(self::T_ELLIPSIS, '...', $this->tokens[$i + 1][2])
                    );
                } else if ('POW' === $matches[1]) {
                    $replace = array(
                        array(self::T_POW, '**', $this->tokens[$i + 1][2])
                    );
                } else if ('POWEQUAL' === $matches[1]) {
                    $replace = array(
                        array(self::T_POW_EQUAL, '**=', $this->tokens[$i + 1][2])
                    );
                } else if ('COALESCE' === $matches[1]) {
                    $replace = array(
                        array(self::T_COALESCE, '??', $this->tokens[$i + 1][2])
                    );
                } else if ('SPACESHIP' === $matches[1]) {
                    $replace = array(
                        array(self::T_SPACESHIP, '<=>', $this->tokens[$i + 1][2]),
                    );
                } else if ('YIELDFROM' === $matches[1]) {
                    $content = hex2bin($matches[2]);
                    $replace = array(
                        array(self::T_YIELD_FROM, $content, $this->tokens[$i + 1][2] - substr_count($content, "\n"))
                    );
                } else {
                    throw new \RuntimeException('Invalid __EMU__ sequence');
                }

                array_splice($this->tokens, $i, 3, $replace);
                $c -= 3 - count($replace);
            // for multichar tokens (e.g. strings) replace any ~__EMU__...~ sequences
            // in their content with the original character sequence
            } elseif (is_array($this->tokens[$i])
                      && 0 !== strpos($this->tokens[$i][1], '__EMU__')
            ) {
                $this->tokens[$i][1] = preg_replace_callback(
                    '(~__EMU__([A-Z]++)__(?:([A-Za-z0-9]++)__)?~)',
                    array($this, 'restoreContentCallback'),
                    $this->tokens[$i][1]
                );
            }
        }
    }

    /*
     * This method is a callback for restoring EMU sequences in
     * multichar tokens (like strings) to their original value.
     */
    public function restoreContentCallback(array $matches) {
        if ('ELLIPSIS' === $matches[1]) {
            return '...';
        } else if ('POW' === $matches[1]) {
            return '**';
        } else if ('POWEQUAL' === $matches[1]) {
            return '**=';
        } else if ('COALESCE' === $matches[1]) {
            return '??';
        } else if ('SPACESHIP' === $matches[1]) {
            return '<=>';
        } else if ('YIELDFROM' === $matches[1]) {
            return hex2bin($matches[2]);
        } else {
            return $matches[0];
        }
    }

    public function getNextToken(&$value = null, &$startAttributes = null, &$endAttributes = null) {
        $token = parent::getNextToken($value, $startAttributes, $endAttributes);

        // replace new keywords by their respective tokens. This is not done
        // if we currently are in an object access (e.g. in $obj->namespace
        // "namespace" stays a T_STRING tokens and isn't converted to T_NAMESPACE)
        if (Tokens::T_STRING === $token && !$this->inObjectAccess) {
            if (isset($this->newKeywords[strtolower($value)])) {
                return $this->newKeywords[strtolower($value)];
            }
        } else {
            // keep track of whether we currently are in an object access (after ->)
            $this->inObjectAccess = Tokens::T_OBJECT_OPERATOR === $token;
        }

        return $token;
    }
}
tokenMap = $this->createTokenMap();

        // map of tokens to drop while lexing (the map is only used for isset lookup,
        // that's why the value is simply set to 1; the value is never actually used.)
        $this->dropTokens = array_fill_keys(
            array(T_WHITESPACE, T_OPEN_TAG, T_COMMENT, T_DOC_COMMENT), 1
        );

        // the usedAttributes member is a map of the used attribute names to a dummy
        // value (here "true")
        $options += array(
            'usedAttributes' => array('comments', 'startLine', 'endLine'),
        );
        $this->usedAttributes = array_fill_keys($options['usedAttributes'], true);
    }

    /**
     * Initializes the lexer for lexing the provided source code.
     *
     * @param string $code The source code to lex
     *
     * @throws Error on lexing errors (unterminated comment or unexpected character)
     */
    public function startLexing($code) {
        $scream = ini_set('xdebug.scream', '0');

        $this->resetErrors();
        $this->tokens = @token_get_all($code);
        $this->handleErrors();

        if (false !== $scream) {
            ini_set('xdebug.scream', $scream);
        }

        $this->code = $code; // keep the code around for __halt_compiler() handling
        $this->pos  = -1;
        $this->line =  1;
        $this->filePos = 0;
    }

    protected function resetErrors() {
        if (function_exists('error_clear_last')) {
            error_clear_last();
        } else {
            // set error_get_last() to defined state by forcing an undefined variable error
            set_error_handler(function() { return false; }, 0);
            @$undefinedVariable;
            restore_error_handler();
        }
    }

    protected function handleErrors() {
        $error = error_get_last();
        if (null === $error) {
            return;
        }

        if (preg_match(
            '~^Unterminated comment starting line ([0-9]+)$~',
            $error['message'], $matches
        )) {
            throw new Error('Unterminated comment', (int) $matches[1]);
        }

        if (preg_match(
            '~^Unexpected character in input:  \'(.)\' \(ASCII=([0-9]+)\)~s',
            $error['message'], $matches
        )) {
            throw new Error(sprintf(
                'Unexpected character "%s" (ASCII %d)',
                $matches[1], $matches[2]
            ));
        }

        // PHP cuts error message after null byte, so need special case
        if (preg_match('~^Unexpected character in input:  \'$~', $error['message'])) {
            throw new Error('Unexpected null byte');
        }
    }

    /**
     * Fetches the next token.
     *
     * The available attributes are determined by the 'usedAttributes' option, which can
     * be specified in the constructor. The following attributes are supported:
     *
     *  * 'comments'      => Array of PhpParser\Comment or PhpParser\Comment\Doc instances,
     *                       representing all comments that occurred between the previous
     *                       non-discarded token and the current one.
     *  * 'startLine'     => Line in which the node starts.
     *  * 'endLine'       => Line in which the node ends.
     *  * 'startTokenPos' => Offset into the token array of the first token in the node.
     *  * 'endTokenPos'   => Offset into the token array of the last token in the node.
     *  * 'startFilePos'  => Offset into the code string of the first character that is part of the node.
     *  * 'endFilePos'    => Offset into the code string of the last character that is part of the node.
     *
     * @param mixed $value           Variable to store token content in
     * @param mixed $startAttributes Variable to store start attributes in
     * @param mixed $endAttributes   Variable to store end attributes in
     *
     * @return int Token id
     */
    public function getNextToken(&$value = null, &$startAttributes = null, &$endAttributes = null) {
        $startAttributes = array();
        $endAttributes   = array();

        while (1) {
            if (isset($this->tokens[++$this->pos])) {
                $token = $this->tokens[$this->pos];
            } else {
                // EOF token with ID 0
                $token = "\0";
            }

            if (isset($this->usedAttributes['startLine'])) {
                $startAttributes['startLine'] = $this->line;
            }
            if (isset($this->usedAttributes['startTokenPos'])) {
                $startAttributes['startTokenPos'] = $this->pos;
            }
            if (isset($this->usedAttributes['startFilePos'])) {
                $startAttributes['startFilePos'] = $this->filePos;
            }

            if (\is_string($token)) {
                $value = $token;
                if (isset($token[1])) {
                    // bug in token_get_all
                    $this->filePos += 2;
                    $id = ord('"');
                } else {
                    $this->filePos += 1;
                    $id = ord($token);
                }
            } elseif (!isset($this->dropTokens[$token[0]])) {
                $value = $token[1];
                $id = $this->tokenMap[$token[0]];

                $this->line += substr_count($value, "\n");
                $this->filePos += \strlen($value);
            } else {
                if (T_COMMENT === $token[0] || T_DOC_COMMENT === $token[0]) {
                    if (isset($this->usedAttributes['comments'])) {
                        $comment = T_DOC_COMMENT === $token[0]
                            ? new Comment\Doc($token[1], $this->line, $this->filePos)
                            : new Comment($token[1], $this->line, $this->filePos);
                        $startAttributes['comments'][] = $comment;
                    }
                }

                $this->line += substr_count($token[1], "\n");
                $this->filePos += \strlen($token[1]);
                continue;
            }

            if (isset($this->usedAttributes['endLine'])) {
                $endAttributes['endLine'] = $this->line;
            }
            if (isset($this->usedAttributes['endTokenPos'])) {
                $endAttributes['endTokenPos'] = $this->pos;
            }
            if (isset($this->usedAttributes['endFilePos'])) {
                $endAttributes['endFilePos'] = $this->filePos - 1;
            }

            return $id;
        }

        throw new \RuntimeException('Reached end of lexer loop');
    }

    /**
     * Returns the token array for current code.
     *
     * The token array is in the same format as provided by the
     * token_get_all() function and does not discard tokens (i.e.
     * whitespace and comments are included). The token position
     * attributes are against this token array.
     *
     * @return array Array of tokens in token_get_all() format
     */
    public function getTokens() {
        return $this->tokens;
    }

    /**
     * Handles __halt_compiler() by returning the text after it.
     *
     * @return string Remaining text
     */
    public function handleHaltCompiler() {
        // text after T_HALT_COMPILER, still including ();
        $textAfter = substr($this->code, $this->filePos);

        // ensure that it is followed by ();
        // this simplifies the situation, by not allowing any comments
        // in between of the tokens.
        if (!preg_match('~^\s*\(\s*\)\s*(?:;|\?>\r?\n?)~', $textAfter, $matches)) {
            throw new Error('__HALT_COMPILER must be followed by "();"');
        }

        // prevent the lexer from returning any further tokens
        $this->pos = count($this->tokens);

        // return with (); removed
        return (string) substr($textAfter, strlen($matches[0])); // (string) converts false to ''
    }

    /**
     * Creates the token map.
     *
     * The token map maps the PHP internal token identifiers
     * to the identifiers used by the Parser. Additionally it
     * maps T_OPEN_TAG_WITH_ECHO to T_ECHO and T_CLOSE_TAG to ';'.
     *
     * @return array The token map
     */
    protected function createTokenMap() {
        $tokenMap = array();

        // 256 is the minimum possible token number, as everything below
        // it is an ASCII value
        for ($i = 256; $i < 1000; ++$i) {
            if (T_DOUBLE_COLON === $i) {
                // T_DOUBLE_COLON is equivalent to T_PAAMAYIM_NEKUDOTAYIM
                $tokenMap[$i] = Tokens::T_PAAMAYIM_NEKUDOTAYIM;
            } elseif(T_OPEN_TAG_WITH_ECHO === $i) {
                // T_OPEN_TAG_WITH_ECHO with dropped T_OPEN_TAG results in T_ECHO
                $tokenMap[$i] = Tokens::T_ECHO;
            } elseif(T_CLOSE_TAG === $i) {
                // T_CLOSE_TAG is equivalent to ';'
                $tokenMap[$i] = ord(';');
            } elseif ('UNKNOWN' !== $name = token_name($i)) {
                if ('T_HASHBANG' === $name) {
                    // HHVM uses a special token for #! hashbang lines
                    $tokenMap[$i] = Tokens::T_INLINE_HTML;
                } else if (defined($name = 'PhpParser\Parser\Tokens::' . $name)) {
                    // Other tokens can be mapped directly
                    $tokenMap[$i] = constant($name);
                }
            }
        }

        // HHVM uses a special token for numbers that overflow to double
        if (defined('T_ONUMBER')) {
            $tokenMap[T_ONUMBER] = Tokens::T_DNUMBER;
        }
        // HHVM also has a separate token for the __COMPILER_HALT_OFFSET__ constant
        if (defined('T_COMPILER_HALT_OFFSET')) {
            $tokenMap[T_COMPILER_HALT_OFFSET] = Tokens::T_STRING;
        }

        return $tokenMap;
    }
}
value = $value;
        $this->byRef = $byRef;
        $this->unpack = $unpack;
    }

    public function getSubNodeNames() {
        return array('value', 'byRef', 'unpack');
    }
}
name = $name;
        $this->value = $value;
    }

    public function getSubNodeNames() {
        return array('name', 'value');
    }
}
items = $items;
    }

    public function getSubNodeNames() {
        return array('items');
    }
}
var = $var;
        $this->dim = $dim;
    }

    public function getSubnodeNames() {
        return array('var', 'dim');
    }
}
key = $key;
        $this->value = $value;
        $this->byRef = $byRef;
    }

    public function getSubNodeNames() {
        return array('key', 'value', 'byRef');
    }
}
var = $var;
        $this->expr = $expr;
    }

    public function getSubNodeNames() {
        return array('var', 'expr');
    }
}
var = $var;
        $this->expr = $expr;
    }

    public function getSubNodeNames() {
        return array('var', 'expr');
    }
}
var = $var;
        $this->expr = $expr;
    }

    public function getSubNodeNames() {
        return array('var', 'expr');
    }
}
left = $left;
        $this->right = $right;
    }

    public function getSubNodeNames() {
        return array('left', 'right');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
class = $class;
        $this->name = $name;
    }

    public function getSubNodeNames() {
        return array('class', 'name');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
 false  : Whether the closure is static
     *                          'byRef'      => false  : Whether to return by reference
     *                          'params'     => array(): Parameters
     *                          'uses'       => array(): use()s
     *                          'returnType' => null   : Return type
     *                          'stmts'      => array(): Statements
     * @param array $attributes Additional attributes
     */
    public function __construct(array $subNodes = array(), array $attributes = array()) {
        parent::__construct($attributes);
        $this->static = isset($subNodes['static']) ? $subNodes['static'] : false;
        $this->byRef = isset($subNodes['byRef']) ? $subNodes['byRef'] : false;
        $this->params = isset($subNodes['params']) ? $subNodes['params'] : array();
        $this->uses = isset($subNodes['uses']) ? $subNodes['uses'] : array();
        $this->returnType = isset($subNodes['returnType']) ? $subNodes['returnType'] : null;
        $this->stmts = isset($subNodes['stmts']) ? $subNodes['stmts'] : array();
    }

    public function getSubNodeNames() {
        return array('static', 'byRef', 'params', 'uses', 'returnType', 'stmts');
    }

    public function returnsByRef() {
        return $this->byRef;
    }

    public function getParams() {
        return $this->params;
    }

    public function getReturnType() {
        return $this->returnType;
    }

    public function getStmts() {
        return $this->stmts;
    }
}
var = $var;
        $this->byRef = $byRef;
    }

    public function getSubNodeNames() {
        return array('var', 'byRef');
    }
}
name = $name;
    }

    public function getSubNodeNames() {
        return array('name');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
name = $name;
        $this->args = $args;
    }

    public function getSubNodeNames() {
        return array('name', 'args');
    }
}
expr = $expr;
        $this->type = $type;
    }

    public function getSubNodeNames() {
        return array('expr', 'type');
    }
}
expr = $expr;
        $this->class = $class;
    }

    public function getSubNodeNames() {
        return array('expr', 'class');
    }
}
vars = $vars;
    }

    public function getSubNodeNames() {
        return array('vars');
    }
}
vars = $vars;
    }

    public function getSubNodeNames() {
        return array('vars');
    }
}
var = $var;
        $this->name = $name;
        $this->args = $args;
    }

    public function getSubNodeNames() {
        return array('var', 'name', 'args');
    }
}
class = $class;
        $this->args = $args;
    }

    public function getSubNodeNames() {
        return array('class', 'args');
    }
}
var = $var;
    }

    public function getSubNodeNames() {
        return array('var');
    }
}
var = $var;
    }

    public function getSubNodeNames() {
        return array('var');
    }
}
var = $var;
    }

    public function getSubNodeNames() {
        return array('var');
    }
}
var = $var;
    }

    public function getSubNodeNames() {
        return array('var');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
var = $var;
        $this->name = $name;
    }

    public function getSubNodeNames() {
        return array('var', 'name');
    }
}
parts = $parts;
    }

    public function getSubNodeNames() {
        return array('parts');
    }
}
class = $class;
        $this->name = $name;
        $this->args = $args;
    }

    public function getSubNodeNames() {
        return array('class', 'name', 'args');
    }
}
class = $class;
        $this->name = $name;
    }

    public function getSubNodeNames() {
        return array('class', 'name');
    }
}
cond = $cond;
        $this->if = $if;
        $this->else = $else;
    }

    public function getSubNodeNames() {
        return array('cond', 'if', 'else');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
name = $name;
    }

    public function getSubNodeNames() {
        return array('name');
    }
}
key = $key;
        $this->value = $value;
    }

    public function getSubNodeNames() {
        return array('key', 'value');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
parts = $parts;
    }

    public function getSubNodeNames() {
        return array('parts');
    }

    /**
     * Gets the first part of the name, i.e. everything before the first namespace separator.
     *
     * @return string First part of the name
     */
    public function getFirst() {
        return $this->parts[0];
    }

    /**
     * Gets the last part of the name, i.e. everything after the last namespace separator.
     *
     * @return string Last part of the name
     */
    public function getLast() {
        return $this->parts[count($this->parts) - 1];
    }

    /**
     * Checks whether the name is unqualified. (E.g. Name)
     *
     * @return bool Whether the name is unqualified
     */
    public function isUnqualified() {
        return 1 == count($this->parts);
    }

    /**
     * Checks whether the name is qualified. (E.g. Name\Name)
     *
     * @return bool Whether the name is qualified
     */
    public function isQualified() {
        return 1 < count($this->parts);
    }

    /**
     * Checks whether the name is fully qualified. (E.g. \Name)
     *
     * @return bool Whether the name is fully qualified
     */
    public function isFullyQualified() {
        return false;
    }

    /**
     * Checks whether the name is explicitly relative to the current namespace. (E.g. namespace\Name)
     *
     * @return bool Whether the name is relative
     */
    public function isRelative() {
        return false;
    }

    /**
     * Returns a string representation of the name by imploding the namespace parts with a separator.
     *
     * @param string $separator The separator to use (defaults to the namespace separator \)
     *
     * @return string String representation
     */
    public function toString($separator = '\\') {
        return implode($separator, $this->parts);
    }

    /**
     * Returns a string representation of the name by imploding the namespace parts with the
     * namespace separator.
     *
     * @return string String representation
     */
    public function __toString() {
        return implode('\\', $this->parts);
    }

    /**
     * Sets the whole name.
     *
     * @deprecated Create a new Name instead, or manually modify the $parts property
     *
     * @param string|array|self $name The name to set the whole name to
     */
    public function set($name) {
        $this->parts = self::prepareName($name);
    }

    /**
     * Prepends a name to this name.
     *
     * @deprecated Use Name::concat($name1, $name2) instead
     *
     * @param string|array|self $name Name to prepend
     */
    public function prepend($name) {
        $this->parts = array_merge(self::prepareName($name), $this->parts);
    }

    /**
     * Appends a name to this name.
     *
     * @deprecated Use Name::concat($name1, $name2) instead
     *
     * @param string|array|self $name Name to append
     */
    public function append($name) {
        $this->parts = array_merge($this->parts, self::prepareName($name));
    }

    /**
     * Sets the first part of the name.
     *
     * @deprecated Use concat($first, $name->slice(1)) instead
     *
     * @param string|array|self $name The name to set the first part to
     */
    public function setFirst($name) {
        array_splice($this->parts, 0, 1, self::prepareName($name));
    }

    /**
     * Sets the last part of the name.
     *
     * @param string|array|self $name The name to set the last part to
     */
    public function setLast($name) {
        array_splice($this->parts, -1, 1, self::prepareName($name));
    }

    /**
     * Gets a slice of a name (similar to array_slice).
     *
     * This method returns a new instance of the same type as the original and with the same
     * attributes.
     *
     * If the slice is empty, a Name with an empty parts array is returned. While this is
     * meaningless in itself, it works correctly in conjunction with concat().
     *
     * @param int $offset Offset to start the slice at
     *
     * @return static Sliced name
     */
    public function slice($offset) {
        // TODO negative offset and length
        if ($offset < 0 || $offset > count($this->parts)) {
            throw new \OutOfBoundsException(sprintf('Offset %d is out of bounds', $offset));
        }

        return new static(array_slice($this->parts, $offset), $this->attributes);
    }

    /**
     * Concatenate two names, yielding a new Name instance.
     *
     * The type of the generated instance depends on which class this method is called on, for
     * example Name\FullyQualified::concat() will yield a Name\FullyQualified instance.
     *
     * @param string|array|self $name1      The first name
     * @param string|array|self $name2      The second name
     * @param array             $attributes Attributes to assign to concatenated name
     *
     * @return static Concatenated name
     */
    public static function concat($name1, $name2, array $attributes = []) {
        return new static(
            array_merge(self::prepareName($name1), self::prepareName($name2)), $attributes
        );
    }

    /**
     * Prepares a (string, array or Name node) name for use in name changing methods by converting
     * it to an array.
     *
     * @param string|array|self $name Name to prepare
     *
     * @return array Prepared name
     */
    private static function prepareName($name) {
        if (is_string($name)) {
            return explode('\\', $name);
        } elseif (is_array($name)) {
            return $name;
        } elseif ($name instanceof self) {
            return $name->parts;
        }

        throw new \InvalidArgumentException(
            'When changing a name you need to pass either a string, an array or a Name node'
        );
    }
}
type = $type;
        $this->byRef = $byRef;
        $this->variadic = $variadic;
        $this->name = $name;
        $this->default = $default;

        if ($variadic && null !== $default) {
            throw new Error('Variadic parameter cannot have a default value', $default->getAttributes());
        }
    }

    public function getSubNodeNames() {
        return array('type', 'byRef', 'variadic', 'name', 'default');
    }
}
value = $value;
    }

    public function getSubNodeNames() {
        return array('value');
    }

    /**
     * @internal
     *
     * Parses a DNUMBER token like PHP would.
     *
     * @param string $str A string number
     *
     * @return float The parsed number
     */
    public static function parse($str) {
        // if string contains any of .eE just cast it to float
        if (false !== strpbrk($str, '.eE')) {
            return (float) $str;
        }

        // otherwise it's an integer notation that overflowed into a float
        // if it starts with 0 it's one of the special integer notations
        if ('0' === $str[0]) {
            // hex
            if ('x' === $str[1] || 'X' === $str[1]) {
                return hexdec($str);
            }

            // bin
            if ('b' === $str[1] || 'B' === $str[1]) {
                return bindec($str);
            }

            // oct
            // substr($str, 0, strcspn($str, '89')) cuts the string at the first invalid digit (8 or 9)
            // so that only the digits before that are used
            return octdec(substr($str, 0, strcspn($str, '89')));
        }

        // dec
        return (float) $str;
    }
}
parts = $parts;
    }

    public function getSubNodeNames() {
        return array('parts');
    }
}
value = $value;
    }

    public function getSubNodeNames() {
        return array('value');
    }
}
value = $value;
    }

    public function getSubNodeNames() {
        return array('value');
    }

    /**
     * Constructs an LNumber node from a string number literal.
     *
     * @param string $str               String number literal (decimal, octal, hex or binary)
     * @param array  $attributes        Additional attributes
     * @param bool   $allowInvalidOctal Whether to allow invalid octal numbers (PHP 5)
     *
     * @return LNumber The constructed LNumber, including kind attribute
     */
    public static function fromString($str, array $attributes = array(), $allowInvalidOctal = false) {
        if ('0' !== $str[0] || '0' === $str) {
            $attributes['kind'] = LNumber::KIND_DEC;
            return new LNumber((int) $str, $attributes);
        }

        if ('x' === $str[1] || 'X' === $str[1]) {
            $attributes['kind'] = LNumber::KIND_HEX;
            return new LNumber(hexdec($str), $attributes);
        }

        if ('b' === $str[1] || 'B' === $str[1]) {
            $attributes['kind'] = LNumber::KIND_BIN;
            return new LNumber(bindec($str), $attributes);
        }

        if (!$allowInvalidOctal && strpbrk($str, '89')) {
            throw new Error('Invalid numeric literal', $attributes);
        }

        // use intval instead of octdec to get proper cutting behavior with malformed numbers
        $attributes['kind'] = LNumber::KIND_OCT;
        return new LNumber(intval($str, 8), $attributes);
    }
}
 '\\',
        '$'  =>  '$',
        'n'  => "\n",
        'r'  => "\r",
        't'  => "\t",
        'f'  => "\f",
        'v'  => "\v",
        'e'  => "\x1B",
    );

    /**
     * Constructs a string scalar node.
     *
     * @param string $value      Value of the string
     * @param array  $attributes Additional attributes
     */
    public function __construct($value, array $attributes = array()) {
        parent::__construct($attributes);
        $this->value = $value;
    }

    public function getSubNodeNames() {
        return array('value');
    }

    /**
     * @internal
     *
     * Parses a string token.
     *
     * @param string $str String token content
     * @param bool $parseUnicodeEscape Whether to parse PHP 7 \u escapes
     *
     * @return string The parsed string
     */
    public static function parse($str, $parseUnicodeEscape = true) {
        $bLength = 0;
        if ('b' === $str[0] || 'B' === $str[0]) {
            $bLength = 1;
        }

        if ('\'' === $str[$bLength]) {
            return str_replace(
                array('\\\\', '\\\''),
                array(  '\\',   '\''),
                substr($str, $bLength + 1, -1)
            );
        } else {
            return self::parseEscapeSequences(
                substr($str, $bLength + 1, -1), '"', $parseUnicodeEscape
            );
        }
    }

    /**
     * @internal
     *
     * Parses escape sequences in strings (all string types apart from single quoted).
     *
     * @param string      $str   String without quotes
     * @param null|string $quote Quote type
     * @param bool $parseUnicodeEscape Whether to parse PHP 7 \u escapes
     *
     * @return string String with escape sequences parsed
     */
    public static function parseEscapeSequences($str, $quote, $parseUnicodeEscape = true) {
        if (null !== $quote) {
            $str = str_replace('\\' . $quote, $quote, $str);
        }

        $extra = '';
        if ($parseUnicodeEscape) {
            $extra = '|u\{([0-9a-fA-F]+)\}';
        }

        return preg_replace_callback(
            '~\\\\([\\\\$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}' . $extra . ')~',
            function($matches) {
                $str = $matches[1];

                if (isset(self::$replacements[$str])) {
                    return self::$replacements[$str];
                } elseif ('x' === $str[0] || 'X' === $str[0]) {
                    return chr(hexdec($str));
                } elseif ('u' === $str[0]) {
                    return self::codePointToUtf8(hexdec($matches[2]));
                } else {
                    return chr(octdec($str));
                }
            },
            $str
        );
    }

    private static function codePointToUtf8($num) {
        if ($num <= 0x7F) {
            return chr($num);
        }
        if ($num <= 0x7FF) {
            return chr(($num>>6) + 0xC0) . chr(($num&0x3F) + 0x80);
        }
        if ($num <= 0xFFFF) {
            return chr(($num>>12) + 0xE0) . chr((($num>>6)&0x3F) + 0x80) . chr(($num&0x3F) + 0x80);
        }
        if ($num <= 0x1FFFFF) {
            return chr(($num>>18) + 0xF0) . chr((($num>>12)&0x3F) + 0x80)
                 . chr((($num>>6)&0x3F) + 0x80) . chr(($num&0x3F) + 0x80);
        }
        throw new Error('Invalid UTF-8 codepoint escape sequence: Codepoint too large');
    }

    /**
     * @internal
     *
     * Parses a constant doc string.
     *
     * @param string $startToken Doc string start token content (<<num = $num;
    }

    public function getSubNodeNames() {
        return array('num');
    }
}
cond = $cond;
        $this->stmts = $stmts;
    }

    public function getSubNodeNames() {
        return array('cond', 'stmts');
    }
}
type = $type;
        $this->var = $var;
        $this->stmts = $stmts;
    }

    public function getSubNodeNames() {
        return array('type', 'var', 'stmts');
    }
}
 true,
        'parent' => true,
        'static' => true,
    );

    /**
     * Constructs a class node.
     *
     * @param string|null $name       Name
     * @param array       $subNodes   Array of the following optional subnodes:
     *                                'type'       => 0      : Type
     *                                'extends'    => null   : Name of extended class
     *                                'implements' => array(): Names of implemented interfaces
     *                                'stmts'      => array(): Statements
     * @param array       $attributes Additional attributes
     */
    public function __construct($name, array $subNodes = array(), array $attributes = array()) {
        parent::__construct($attributes);
        $this->type = isset($subNodes['type']) ? $subNodes['type'] : 0;
        $this->name = $name;
        $this->extends = isset($subNodes['extends']) ? $subNodes['extends'] : null;
        $this->implements = isset($subNodes['implements']) ? $subNodes['implements'] : array();
        $this->stmts = isset($subNodes['stmts']) ? $subNodes['stmts'] : array();

        if (null !== $this->name && isset(self::$specialNames[strtolower($this->name)])) {
            throw new Error(sprintf('Cannot use \'%s\' as class name as it is reserved', $this->name));
        }

        if (isset(self::$specialNames[strtolower($this->extends)])) {
            throw new Error(
                sprintf('Cannot use \'%s\' as class name as it is reserved', $this->extends),
                $this->extends->getAttributes()
            );
        }

        foreach ($this->implements as $interface) {
            if (isset(self::$specialNames[strtolower($interface)])) {
                throw new Error(
                    sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
                    $interface->getAttributes()
                );
            }
        }
    }

    public function getSubNodeNames() {
        return array('type', 'name', 'extends', 'implements', 'stmts');
    }

    public function isAbstract() {
        return (bool) ($this->type & self::MODIFIER_ABSTRACT);
    }

    public function isFinal() {
        return (bool) ($this->type & self::MODIFIER_FINAL);
    }

    public function isAnonymous() {
        return null === $this->name;
    }

    /**
     * @internal
     */
    public static function verifyModifier($a, $b) {
        if ($a & self::VISIBILITY_MODIFER_MASK && $b & self::VISIBILITY_MODIFER_MASK) {
            throw new Error('Multiple access type modifiers are not allowed');
        }

        if ($a & self::MODIFIER_ABSTRACT && $b & self::MODIFIER_ABSTRACT) {
            throw new Error('Multiple abstract modifiers are not allowed');
        }

        if ($a & self::MODIFIER_STATIC && $b & self::MODIFIER_STATIC) {
            throw new Error('Multiple static modifiers are not allowed');
        }

        if ($a & self::MODIFIER_FINAL && $b & self::MODIFIER_FINAL) {
            throw new Error('Multiple final modifiers are not allowed');
        }

        if ($a & 48 && $b & 48) {
            throw new Error('Cannot use the final modifier on an abstract class member');
        }
    }
}
consts = $consts;
    }

    public function getSubNodeNames() {
        return array('consts');
    }
}
stmts as $stmt) {
            if ($stmt instanceof ClassMethod) {
                $methods[] = $stmt;
            }
        }
        return $methods;
    }

    /**
     * Gets method with the given name defined directly in this class/interface/trait.
     *
     * @param string $name Name of the method (compared case-insensitively)
     *
     * @return ClassMethod|null Method node or null if the method does not exist
     */
    public function getMethod($name) {
        $lowerName = strtolower($name);
        foreach ($this->stmts as $stmt) {
            if ($stmt instanceof ClassMethod && $lowerName === strtolower($stmt->name)) {
                return $stmt;
            }
        }
        return null;
    }
}
 MODIFIER_PUBLIC: Type
     *                                'byRef'      => false          : Whether to return by reference
     *                                'params'     => array()        : Parameters
     *                                'returnType' => null           : Return type
     *                                'stmts'      => array()        : Statements
     * @param array       $attributes Additional attributes
     */
    public function __construct($name, array $subNodes = array(), array $attributes = array()) {
        parent::__construct($attributes);
        $this->type = isset($subNodes['type']) ? $subNodes['type'] : 0;
        $this->byRef = isset($subNodes['byRef'])  ? $subNodes['byRef']  : false;
        $this->name = $name;
        $this->params = isset($subNodes['params']) ? $subNodes['params'] : array();
        $this->returnType = isset($subNodes['returnType']) ? $subNodes['returnType'] : null;
        $this->stmts = array_key_exists('stmts', $subNodes) ? $subNodes['stmts'] : array();

        if ($this->type & Class_::MODIFIER_STATIC) {
            switch (strtolower($this->name)) {
                case '__construct':
                    throw new Error(sprintf('Constructor %s() cannot be static', $this->name));
                case '__destruct':
                    throw new Error(sprintf('Destructor %s() cannot be static', $this->name));
                case '__clone':
                    throw new Error(sprintf('Clone method %s() cannot be static', $this->name));
            }
        }
    }

    public function getSubNodeNames() {
        return array('type', 'byRef', 'name', 'params', 'returnType', 'stmts');
    }

    public function returnsByRef() {
        return $this->byRef;
    }

    public function getParams() {
        return $this->params;
    }

    public function getReturnType() {
        return $this->returnType;
    }

    public function getStmts() {
        return $this->stmts;
    }

    public function isPublic() {
        return ($this->type & Class_::MODIFIER_PUBLIC) !== 0
            || ($this->type & Class_::VISIBILITY_MODIFER_MASK) === 0;
    }

    public function isProtected() {
        return (bool) ($this->type & Class_::MODIFIER_PROTECTED);
    }

    public function isPrivate() {
        return (bool) ($this->type & Class_::MODIFIER_PRIVATE);
    }

    public function isAbstract() {
        return (bool) ($this->type & Class_::MODIFIER_ABSTRACT);
    }

    public function isFinal() {
        return (bool) ($this->type & Class_::MODIFIER_FINAL);
    }

    public function isStatic() {
        return (bool) ($this->type & Class_::MODIFIER_STATIC);
    }
}
consts = $consts;
    }

    public function getSubNodeNames() {
        return array('consts');
    }
}
num = $num;
    }

    public function getSubNodeNames() {
        return array('num');
    }
}
declares = $declares;
        $this->stmts = $stmts;
    }

    public function getSubNodeNames() {
        return array('declares', 'stmts');
    }
}
value pair node.
     *
     * @param string    $key        Key
     * @param Node\Expr $value      Value
     * @param array     $attributes Additional attributes
     */
    public function __construct($key, Node\Expr $value, array $attributes = array()) {
        parent::__construct($attributes);
        $this->key = $key;
        $this->value = $value;
    }

    public function getSubNodeNames() {
        return array('key', 'value');
    }
}
cond = $cond;
        $this->stmts = $stmts;
    }

    public function getSubNodeNames() {
        return array('cond', 'stmts');
    }
}
exprs = $exprs;
    }

    public function getSubNodeNames() {
        return array('exprs');
    }
}
stmts = $stmts;
    }

    public function getSubNodeNames() {
        return array('stmts');
    }
}
cond = $cond;
        $this->stmts = $stmts;
    }

    public function getSubNodeNames() {
        return array('cond', 'stmts');
    }
}
 array(): Init expressions
     *                          'cond'  => array(): Loop conditions
     *                          'loop'  => array(): Loop expressions
     *                          'stmts' => array(): Statements
     * @param array $attributes Additional attributes
     */
    public function __construct(array $subNodes = array(), array $attributes = array()) {
        parent::__construct($attributes);
        $this->init = isset($subNodes['init']) ? $subNodes['init'] : array();
        $this->cond = isset($subNodes['cond']) ? $subNodes['cond'] : array();
        $this->loop = isset($subNodes['loop']) ? $subNodes['loop'] : array();
        $this->stmts = isset($subNodes['stmts']) ? $subNodes['stmts'] : array();
    }

    public function getSubNodeNames() {
        return array('init', 'cond', 'loop', 'stmts');
    }
}
 null   : Variable to assign key to
     *                              'byRef'  => false  : Whether to assign value by reference
     *                              'stmts'  => array(): Statements
     * @param array     $attributes Additional attributes
     */
    public function __construct(Node\Expr $expr, Node\Expr $valueVar, array $subNodes = array(), array $attributes = array()) {
        parent::__construct($attributes);
        $this->expr = $expr;
        $this->keyVar = isset($subNodes['keyVar']) ? $subNodes['keyVar'] : null;
        $this->byRef = isset($subNodes['byRef']) ? $subNodes['byRef'] : false;
        $this->valueVar = $valueVar;
        $this->stmts = isset($subNodes['stmts']) ? $subNodes['stmts'] : array();
    }

    public function getSubNodeNames() {
        return array('expr', 'keyVar', 'byRef', 'valueVar', 'stmts');
    }
}
 false  : Whether to return by reference
     *                           'params'     => array(): Parameters
     *                           'returnType' => null   : Return type
     *                           'stmts'      => array(): Statements
     * @param array  $attributes Additional attributes
     */
    public function __construct($name, array $subNodes = array(), array $attributes = array()) {
        parent::__construct($attributes);
        $this->byRef = isset($subNodes['byRef']) ? $subNodes['byRef'] : false;
        $this->name = $name;
        $this->params = isset($subNodes['params']) ? $subNodes['params'] : array();
        $this->returnType = isset($subNodes['returnType']) ? $subNodes['returnType'] : null;
        $this->stmts = isset($subNodes['stmts']) ? $subNodes['stmts'] : array();
    }

    public function getSubNodeNames() {
        return array('byRef', 'name', 'params', 'returnType', 'stmts');
    }

    public function returnsByRef() {
        return $this->byRef;
    }

    public function getParams() {
        return $this->params;
    }

    public function getReturnType() {
        return $this->returnType;
    }

    public function getStmts() {
        return $this->stmts;
    }
}
vars = $vars;
    }

    public function getSubNodeNames() {
        return array('vars');
    }
}
name = $name;
    }

    public function getSubNodeNames() {
        return array('name');
    }
}
type = $type;
        $this->prefix = $prefix;
        $this->uses = $uses;
    }

    public function getSubNodeNames() {
        return array('type', 'prefix', 'uses');
    }
}
remaining = $remaining;
    }

    public function getSubNodeNames() {
        return array('remaining');
    }
}
 array(): Statements
     *                              'elseifs' => array(): Elseif clauses
     *                              'else'    => null   : Else clause
     * @param array     $attributes Additional attributes
     */
    public function __construct(Node\Expr $cond, array $subNodes = array(), array $attributes = array()) {
        parent::__construct($attributes);
        $this->cond = $cond;
        $this->stmts = isset($subNodes['stmts']) ? $subNodes['stmts'] : array();
        $this->elseifs = isset($subNodes['elseifs']) ? $subNodes['elseifs'] : array();
        $this->else = isset($subNodes['else']) ? $subNodes['else'] : null;
    }

    public function getSubNodeNames() {
        return array('cond', 'stmts', 'elseifs', 'else');
    }
}
value = $value;
    }

    public function getSubNodeNames() {
        return array('value');
    }
}
 true,
        'parent' => true,
        'static' => true,
    );

    /**
     * Constructs a class node.
     *
     * @param string $name       Name
     * @param array  $subNodes   Array of the following optional subnodes:
     *                           'extends' => array(): Name of extended interfaces
     *                           'stmts'   => array(): Statements
     * @param array  $attributes Additional attributes
     */
    public function __construct($name, array $subNodes = array(), array $attributes = array()) {
        parent::__construct($attributes);
        $this->name = $name;
        $this->extends = isset($subNodes['extends']) ? $subNodes['extends'] : array();
        $this->stmts = isset($subNodes['stmts']) ? $subNodes['stmts'] : array();

        if (isset(self::$specialNames[strtolower($this->name)])) {
            throw new Error(sprintf('Cannot use \'%s\' as class name as it is reserved', $this->name));
        }

        foreach ($this->extends as $interface) {
            if (isset(self::$specialNames[strtolower($interface)])) {
                throw new Error(
                    sprintf('Cannot use \'%s\' as interface name as it is reserved', $interface),
                    $interface->getAttributes()
                );
            }
        }
    }

    public function getSubNodeNames() {
        return array('name', 'extends', 'stmts');
    }
}
name = $name;
    }

    public function getSubNodeNames() {
        return array('name');
    }
}
 true,
        'parent' => true,
        'static' => true,
    );

    /**
     * Constructs a namespace node.
     *
     * @param null|Node\Name $name       Name
     * @param null|Node[]    $stmts      Statements
     * @param array          $attributes Additional attributes
     */
    public function __construct(Node\Name $name = null, $stmts = array(), array $attributes = array()) {
        parent::__construct($attributes);
        $this->name = $name;
        $this->stmts = $stmts;

        if (isset(self::$specialNames[strtolower($this->name)])) {
            throw new Error(
                sprintf('Cannot use \'%s\' as namespace name', $this->name),
                $this->name->getAttributes()
            );
        }

        if (null !== $this->stmts) {
            foreach ($this->stmts as $stmt) {
                if ($stmt instanceof self) {
                    throw new Error('Namespace declarations cannot be nested', $stmt->getAttributes());
                }
            }
        }
    }

    public function getSubNodeNames() {
        return array('name', 'stmts');
    }
}
type = $type;
        $this->props = $props;
    }

    public function getSubNodeNames() {
        return array('type', 'props');
    }

    public function isPublic() {
        return ($this->type & Class_::MODIFIER_PUBLIC) !== 0
            || ($this->type & Class_::VISIBILITY_MODIFER_MASK) === 0;
    }

    public function isProtected() {
        return (bool) ($this->type & Class_::MODIFIER_PROTECTED);
    }

    public function isPrivate() {
        return (bool) ($this->type & Class_::MODIFIER_PRIVATE);
    }

    public function isStatic() {
        return (bool) ($this->type & Class_::MODIFIER_STATIC);
    }
}
name = $name;
        $this->default = $default;
    }

    public function getSubNodeNames() {
        return array('name', 'default');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
vars = $vars;
    }

    public function getSubNodeNames() {
        return array('vars');
    }
}
name = $name;
        $this->default = $default;
    }

    public function getSubNodeNames() {
        return array('name', 'default');
    }
}
cond = $cond;
        $this->cases = $cases;
    }

    public function getSubNodeNames() {
        return array('cond', 'cases');
    }
}
expr = $expr;
    }

    public function getSubNodeNames() {
        return array('expr');
    }
}
name = $name;
        $this->stmts = $stmts;
    }

    public function getSubNodeNames() {
        return array('name', 'stmts');
    }
}
traits = $traits;
        $this->adaptations = $adaptations;
    }

    public function getSubNodeNames() {
        return array('traits', 'adaptations');
    }
}
trait = $trait;
        $this->method = $method;
        $this->newModifier = $newModifier;
        $this->newName = $newName;
    }

    public function getSubNodeNames() {
        return array('trait', 'method', 'newModifier', 'newName');
    }
}
trait = $trait;
        $this->method = $method;
        $this->insteadof = $insteadof;
    }

    public function getSubNodeNames() {
        return array('trait', 'method', 'insteadof');
    }
}
stmts = $stmts;
        $this->catches = $catches;
        $this->finallyStmts = $finallyStmts;
    }

    public function getSubNodeNames() {
        return array('stmts', 'catches', 'finallyStmts');
    }
}
vars = $vars;
    }

    public function getSubNodeNames() {
        return array('vars');
    }
}
type = $type;
        $this->uses = $uses;
    }

    public function getSubNodeNames() {
        return array('type', 'uses');
    }
}
getLast();
        }

        if ('self' == strtolower($alias) || 'parent' == strtolower($alias)) {
            throw new Error(sprintf(
                'Cannot use %s as %s because \'%2$s\' is a special class name',
                $name, $alias
            ));
        }

        parent::__construct($attributes);
        $this->type = $type;
        $this->name = $name;
        $this->alias = $alias;
    }

    public function getSubNodeNames() {
        return array('type', 'name', 'alias');
    }
}
cond = $cond;
        $this->stmts = $stmts;
    }

    public function getSubNodeNames() {
        return array('cond', 'stmts');
    }
}
attributes = $attributes;
    }

    /**
     * Gets the type of the node.
     *
     * @return string Type of the node
     */
    public function getType() {
        return strtr(substr(rtrim(get_class($this), '_'), 15), '\\', '_');
    }

    /**
     * Gets line the node started in.
     *
     * @return int Line
     */
    public function getLine() {
        return $this->getAttribute('startLine', -1);
    }

    /**
     * Sets line the node started in.
     *
     * @param int $line Line
     */
    public function setLine($line) {
        $this->setAttribute('startLine', (int) $line);
    }

    /**
     * Gets the doc comment of the node.
     *
     * The doc comment has to be the last comment associated with the node.
     *
     * @return null|Comment\Doc Doc comment object or null
     */
    public function getDocComment() {
        $comments = $this->getAttribute('comments');
        if (!$comments) {
            return null;
        }

        $lastComment = $comments[count($comments) - 1];
        if (!$lastComment instanceof Comment\Doc) {
            return null;
        }

        return $lastComment;
    }

    public function setAttribute($key, $value) {
        $this->attributes[$key] = $value;
    }

    public function hasAttribute($key) {
        return array_key_exists($key, $this->attributes);
    }

    public function &getAttribute($key, $default = null) {
        if (!array_key_exists($key, $this->attributes)) {
            return $default;
        } else {
            return $this->attributes[$key];
        }
    }

    public function getAttributes() {
        return $this->attributes;
    }
}
dumpComments = !empty($options['dumpComments']);
    }

    /**
     * Dumps a node or array.
     *
     * @param array|Node $node Node or array to dump
     *
     * @return string Dumped value
     */
    public function dump($node) {
        if ($node instanceof Node) {
            $r = $node->getType() . '(';

            foreach ($node->getSubNodeNames() as $key) {
                $r .= "\n    " . $key . ': ';

                $value = $node->$key;
                if (null === $value) {
                    $r .= 'null';
                } elseif (false === $value) {
                    $r .= 'false';
                } elseif (true === $value) {
                    $r .= 'true';
                } elseif (is_scalar($value)) {
                    $r .= $value;
                } else {
                    $r .= str_replace("\n", "\n    ", $this->dump($value));
                }
            }

            if ($this->dumpComments && $comments = $node->getAttribute('comments')) {
                $r .= "\n    comments: " . str_replace("\n", "\n    ", $this->dump($comments));
            }
        } elseif (is_array($node)) {
            $r = 'array(';

            foreach ($node as $key => $value) {
                $r .= "\n    " . $key . ': ';

                if (null === $value) {
                    $r .= 'null';
                } elseif (false === $value) {
                    $r .= 'false';
                } elseif (true === $value) {
                    $r .= 'true';
                } elseif (is_scalar($value)) {
                    $r .= $value;
                } else {
                    $r .= str_replace("\n", "\n    ", $this->dump($value));
                }
            }
        } elseif ($node instanceof Comment) {
            return $node->getReformattedText();
        } else {
            throw new \InvalidArgumentException('Can only dump nodes and arrays.');
        }

        return $r . "\n)";
    }
}
visitors = array();
        $this->cloneNodes = $cloneNodes;
    }

    /**
     * Adds a visitor.
     *
     * @param NodeVisitor $visitor Visitor to add
     */
    public function addVisitor(NodeVisitor $visitor) {
        $this->visitors[] = $visitor;
    }

    /**
     * Removes an added visitor.
     *
     * @param NodeVisitor $visitor
     */
    public function removeVisitor(NodeVisitor $visitor) {
        foreach ($this->visitors as $index => $storedVisitor) {
            if ($storedVisitor === $visitor) {
                unset($this->visitors[$index]);
                break;
            }
        }
    }

    /**
     * Traverses an array of nodes using the registered visitors.
     *
     * @param Node[] $nodes Array of nodes
     *
     * @return Node[] Traversed array of nodes
     */
    public function traverse(array $nodes) {
        foreach ($this->visitors as $visitor) {
            if (null !== $return = $visitor->beforeTraverse($nodes)) {
                $nodes = $return;
            }
        }

        $nodes = $this->traverseArray($nodes);

        foreach ($this->visitors as $visitor) {
            if (null !== $return = $visitor->afterTraverse($nodes)) {
                $nodes = $return;
            }
        }

        return $nodes;
    }

    protected function traverseNode(Node $node) {
        if ($this->cloneNodes) {
            $node = clone $node;
        }

        foreach ($node->getSubNodeNames() as $name) {
            $subNode =& $node->$name;

            if (is_array($subNode)) {
                $subNode = $this->traverseArray($subNode);
            } elseif ($subNode instanceof Node) {
                $traverseChildren = true;
                foreach ($this->visitors as $visitor) {
                    $return = $visitor->enterNode($subNode);
                    if (self::DONT_TRAVERSE_CHILDREN === $return) {
                        $traverseChildren = false;
                    } else if (null !== $return) {
                        $subNode = $return;
                    }
                }

                if ($traverseChildren) {
                    $subNode = $this->traverseNode($subNode);
                }

                foreach ($this->visitors as $visitor) {
                    if (null !== $return = $visitor->leaveNode($subNode)) {
                        if (is_array($return)) {
                            throw new \LogicException(
                                'leaveNode() may only return an array ' .
                                'if the parent structure is an array'
                            );
                        }
                        $subNode = $return;
                    }
                }
            }
        }

        return $node;
    }

    protected function traverseArray(array $nodes) {
        $doNodes = array();

        foreach ($nodes as $i => &$node) {
            if (is_array($node)) {
                $node = $this->traverseArray($node);
            } elseif ($node instanceof Node) {
                $traverseChildren = true;
                foreach ($this->visitors as $visitor) {
                    $return = $visitor->enterNode($node);
                    if (self::DONT_TRAVERSE_CHILDREN === $return) {
                        $traverseChildren = false;
                    } else if (null !== $return) {
                        $node = $return;
                    }
                }

                if ($traverseChildren) {
                    $node = $this->traverseNode($node);
                }

                foreach ($this->visitors as $visitor) {
                    $return = $visitor->leaveNode($node);

                    if (self::REMOVE_NODE === $return) {
                        $doNodes[] = array($i, array());
                        break;
                    } elseif (is_array($return)) {
                        $doNodes[] = array($i, $return);
                        break;
                    } elseif (null !== $return) {
                        $node = $return;
                    }
                }
            }
        }

        if (!empty($doNodes)) {
            while (list($i, $replace) = array_pop($doNodes)) {
                array_splice($nodes, $i, 1, $replace);
            }
        }

        return $nodes;
    }
}
 [aliasName => originalName]] */
    protected $aliases;

    public function beforeTraverse(array $nodes) {
        $this->resetState();
    }

    public function enterNode(Node $node) {
        if ($node instanceof Stmt\Namespace_) {
            $this->resetState($node->name);
        } elseif ($node instanceof Stmt\Use_) {
            foreach ($node->uses as $use) {
                $this->addAlias($use, $node->type, null);
            }
        } elseif ($node instanceof Stmt\GroupUse) {
            foreach ($node->uses as $use) {
                $this->addAlias($use, $node->type, $node->prefix);
            }
        } elseif ($node instanceof Stmt\Class_) {
            if (null !== $node->extends) {
                $node->extends = $this->resolveClassName($node->extends);
            }

            foreach ($node->implements as &$interface) {
                $interface = $this->resolveClassName($interface);
            }

            if (null !== $node->name) {
                $this->addNamespacedName($node);
            }
        } elseif ($node instanceof Stmt\Interface_) {
            foreach ($node->extends as &$interface) {
                $interface = $this->resolveClassName($interface);
            }

            $this->addNamespacedName($node);
        } elseif ($node instanceof Stmt\Trait_) {
            $this->addNamespacedName($node);
        } elseif ($node instanceof Stmt\Function_) {
            $this->addNamespacedName($node);
            $this->resolveSignature($node);
        } elseif ($node instanceof Stmt\ClassMethod
                  || $node instanceof Expr\Closure
        ) {
            $this->resolveSignature($node);
        } elseif ($node instanceof Stmt\Const_) {
            foreach ($node->consts as $const) {
                $this->addNamespacedName($const);
            }
        } elseif ($node instanceof Expr\StaticCall
                  || $node instanceof Expr\StaticPropertyFetch
                  || $node instanceof Expr\ClassConstFetch
                  || $node instanceof Expr\New_
                  || $node instanceof Expr\Instanceof_
        ) {
            if ($node->class instanceof Name) {
                $node->class = $this->resolveClassName($node->class);
            }
        } elseif ($node instanceof Stmt\Catch_) {
            $node->type = $this->resolveClassName($node->type);
        } elseif ($node instanceof Expr\FuncCall) {
            if ($node->name instanceof Name) {
                $node->name = $this->resolveOtherName($node->name, Stmt\Use_::TYPE_FUNCTION);
            }
        } elseif ($node instanceof Expr\ConstFetch) {
            $node->name = $this->resolveOtherName($node->name, Stmt\Use_::TYPE_CONSTANT);
        } elseif ($node instanceof Stmt\TraitUse) {
            foreach ($node->traits as &$trait) {
                $trait = $this->resolveClassName($trait);
            }

            foreach ($node->adaptations as $adaptation) {
                if (null !== $adaptation->trait) {
                    $adaptation->trait = $this->resolveClassName($adaptation->trait);
                }

                if ($adaptation instanceof Stmt\TraitUseAdaptation\Precedence) {
                    foreach ($adaptation->insteadof as &$insteadof) {
                        $insteadof = $this->resolveClassName($insteadof);
                    }
                }
            }

        }
    }

    protected function resetState(Name $namespace = null) {
        $this->namespace = $namespace;
        $this->aliases   = array(
            Stmt\Use_::TYPE_NORMAL   => array(),
            Stmt\Use_::TYPE_FUNCTION => array(),
            Stmt\Use_::TYPE_CONSTANT => array(),
        );
    }

    protected function addAlias(Stmt\UseUse $use, $type, Name $prefix = null) {
        // Add prefix for group uses
        $name = $prefix ? Name::concat($prefix, $use->name) : $use->name;
        // Type is determined either by individual element or whole use declaration
        $type |= $use->type;

        // Constant names are case sensitive, everything else case insensitive
        if ($type === Stmt\Use_::TYPE_CONSTANT) {
            $aliasName = $use->alias;
        } else {
            $aliasName = strtolower($use->alias);
        }

        if (isset($this->aliases[$type][$aliasName])) {
            $typeStringMap = array(
                Stmt\Use_::TYPE_NORMAL   => '',
                Stmt\Use_::TYPE_FUNCTION => 'function ',
                Stmt\Use_::TYPE_CONSTANT => 'const ',
            );

            throw new Error(
                sprintf(
                    'Cannot use %s%s as %s because the name is already in use',
                    $typeStringMap[$type], $name, $use->alias
                ),
                $use->getLine()
            );
        }

        $this->aliases[$type][$aliasName] = $name;
    }

    /** @param Stmt\Function_|Stmt\ClassMethod|Expr\Closure $node */
    private function resolveSignature($node) {
        foreach ($node->params as $param) {
            if ($param->type instanceof Name) {
                $param->type = $this->resolveClassName($param->type);
            }
        }
        if ($node->returnType instanceof Name) {
            $node->returnType = $this->resolveClassName($node->returnType);
        }
    }

    protected function resolveClassName(Name $name) {
        // don't resolve special class names
        if (in_array(strtolower($name->toString()), array('self', 'parent', 'static'))) {
            if (!$name->isUnqualified()) {
                throw new Error(
                    sprintf("'\\%s' is an invalid class name", $name->toString()),
                    $name->getLine()
                );
            }

            return $name;
        }

        // fully qualified names are already resolved
        if ($name->isFullyQualified()) {
            return $name;
        }

        $aliasName = strtolower($name->getFirst());
        if (!$name->isRelative() && isset($this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName])) {
            // resolve aliases (for non-relative names)
            $alias = $this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName];
            return FullyQualified::concat($alias, $name->slice(1), $name->getAttributes());
        }

        if (null !== $this->namespace) {
            // if no alias exists prepend current namespace
            return FullyQualified::concat($this->namespace, $name, $name->getAttributes());
        }

        return new FullyQualified($name->parts, $name->getAttributes());
    }

    protected function resolveOtherName(Name $name, $type) {
        // fully qualified names are already resolved
        if ($name->isFullyQualified()) {
            return $name;
        }

        // resolve aliases for qualified names
        $aliasName = strtolower($name->getFirst());
        if ($name->isQualified() && isset($this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName])) {
            $alias = $this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName];
            return FullyQualified::concat($alias, $name->slice(1), $name->getAttributes());
        }

        if ($name->isUnqualified()) {
            if ($type === Stmt\Use_::TYPE_CONSTANT) {
                // constant aliases are case-sensitive, function aliases case-insensitive
                $aliasName = $name->getFirst();
            }

            if (!isset($this->aliases[$type][$aliasName])) {
                // unqualified, unaliased names cannot be resolved at compile-time
                return $name;
            }

            // resolve unqualified aliases
            return new FullyQualified($this->aliases[$type][$aliasName], $name->getAttributes());
        }

        if (null !== $this->namespace) {
            // if no alias exists prepend current namespace
            return FullyQualified::concat($this->namespace, $name, $name->getAttributes());
        }

        return new FullyQualified($name->parts, $name->getAttributes());
    }

    protected function addNamespacedName(Node $node) {
        if (null !== $this->namespace) {
            $node->namespacedName = Name::concat($this->namespace, $node->name);
        } else {
            $node->namespacedName = new Name($node->name);
        }
    }
}
parsers = $parsers;
        $this->errors = [];
    }

    public function parse($code) {
        list($firstStmts, $firstErrors, $firstError) = $this->tryParse($this->parsers[0], $code);
        if ($firstErrors === []) {
            $this->errors = [];
            return $firstStmts;
        }

        for ($i = 1, $c = count($this->parsers); $i < $c; ++$i) {
            list($stmts, $errors) = $this->tryParse($this->parsers[$i], $code);
            if ($errors === []) {
                $this->errors = [];
                return $stmts;
            }
        }

        $this->errors = $firstErrors;
        if ($firstError) {
            throw $firstError;
        }
        return $firstStmts;
    }

    public function getErrors() {
        return $this->errors;
    }

    private function tryParse(Parser $parser, $code) {
        $stmts = null;
        $error = null;
        try {
            $stmts = $parser->parse($code);
        } catch (Error $error) {}
        $errors = $parser->getErrors();
        return [$stmts, $errors, $error];
    }
}
'",
        "T_IS_GREATER_OR_EQUAL",
        "T_SL",
        "T_SR",
        "'+'",
        "'-'",
        "'.'",
        "'*'",
        "'/'",
        "'%'",
        "'!'",
        "T_INSTANCEOF",
        "'~'",
        "T_INC",
        "T_DEC",
        "T_INT_CAST",
        "T_DOUBLE_CAST",
        "T_STRING_CAST",
        "T_ARRAY_CAST",
        "T_OBJECT_CAST",
        "T_BOOL_CAST",
        "T_UNSET_CAST",
        "'@'",
        "T_POW",
        "'['",
        "T_NEW",
        "T_CLONE",
        "T_EXIT",
        "T_IF",
        "T_ELSEIF",
        "T_ELSE",
        "T_ENDIF",
        "T_LNUMBER",
        "T_DNUMBER",
        "T_STRING",
        "T_STRING_VARNAME",
        "T_VARIABLE",
        "T_NUM_STRING",
        "T_INLINE_HTML",
        "T_ENCAPSED_AND_WHITESPACE",
        "T_CONSTANT_ENCAPSED_STRING",
        "T_ECHO",
        "T_DO",
        "T_WHILE",
        "T_ENDWHILE",
        "T_FOR",
        "T_ENDFOR",
        "T_FOREACH",
        "T_ENDFOREACH",
        "T_DECLARE",
        "T_ENDDECLARE",
        "T_AS",
        "T_SWITCH",
        "T_ENDSWITCH",
        "T_CASE",
        "T_DEFAULT",
        "T_BREAK",
        "T_CONTINUE",
        "T_GOTO",
        "T_FUNCTION",
        "T_CONST",
        "T_RETURN",
        "T_TRY",
        "T_CATCH",
        "T_FINALLY",
        "T_THROW",
        "T_USE",
        "T_INSTEADOF",
        "T_GLOBAL",
        "T_STATIC",
        "T_ABSTRACT",
        "T_FINAL",
        "T_PRIVATE",
        "T_PROTECTED",
        "T_PUBLIC",
        "T_VAR",
        "T_UNSET",
        "T_ISSET",
        "T_EMPTY",
        "T_HALT_COMPILER",
        "T_CLASS",
        "T_TRAIT",
        "T_INTERFACE",
        "T_EXTENDS",
        "T_IMPLEMENTS",
        "T_OBJECT_OPERATOR",
        "T_LIST",
        "T_ARRAY",
        "T_CALLABLE",
        "T_CLASS_C",
        "T_TRAIT_C",
        "T_METHOD_C",
        "T_FUNC_C",
        "T_LINE",
        "T_FILE",
        "T_START_HEREDOC",
        "T_END_HEREDOC",
        "T_DOLLAR_OPEN_CURLY_BRACES",
        "T_CURLY_OPEN",
        "T_PAAMAYIM_NEKUDOTAYIM",
        "T_NAMESPACE",
        "T_NS_C",
        "T_DIR",
        "T_NS_SEPARATOR",
        "T_ELLIPSIS",
        "';'",
        "'{'",
        "'}'",
        "'('",
        "')'",
        "'$'",
        "'`'",
        "']'",
        "'\"'"
    );

    protected $tokenToSymbol = array(
            0,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,   53,  156,  157,  153,   52,   35,  157,
          151,  152,   50,   47,    7,   48,   49,   51,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,   29,  148,
           41,   15,   43,   28,   65,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,   67,  157,  155,   34,  157,  154,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  149,   33,  150,   55,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,    1,    2,    3,    4,
            5,    6,    8,    9,   10,   11,   12,   13,   14,   16,
           17,   18,   19,   20,   21,   22,   23,   24,   25,   26,
           27,   30,   31,   32,   36,   37,   38,   39,   40,   42,
           44,   45,   46,   54,   56,   57,   58,   59,   60,   61,
           62,   63,   64,   66,   68,   69,   70,   71,   72,   73,
           74,   75,   76,   77,   78,   79,   80,   81,  157,  157,
           82,   83,   84,   85,   86,   87,   88,   89,   90,   91,
           92,   93,   94,   95,   96,   97,   98,   99,  100,  101,
          102,  103,  104,  105,  106,  107,  108,  109,  110,  111,
          112,  113,  114,  115,  116,  117,  118,  119,  120,  121,
          122,  123,  124,  125,  126,  127,  128,  129,  130,  131,
          132,  133,  134,  135,  136,  137,  157,  157,  157,  157,
          157,  157,  138,  139,  140,  141,  142,  143,  144,  145,
          146,  147
    );

    protected $action = array(
          672,  673,  674,  675,  676,-32766,  677,  678,  679,  715,
          716,  216,  217,  218,  219,  220,  221,  222,  223,  224,
          282,  225,  226,  227,  228,  229,  230,  231,  232,  233,
          234,  235,  236,-32766,-32766,-32766,-32766,-32766,-32766,-32766,
        -32766,-32767,-32767,-32767,-32767,  356,  237,  238,-32766,-32766,
        -32766,-32766,  680,-32766,    0,-32766,-32766,-32766,-32766,-32766,
        -32766,-32767,-32767,-32767,-32767,-32767,  681,  682,  683,  684,
          685,  686,  687, 1178,  204,  747,-32766,-32766,-32766,-32766,
        -32766,   23,  688,  689,  690,  691,  692,  693,  694,  695,
          696,  697,  698,  718,  719,  720,  721,  722,  710,  711,
          712,  713,  714,  699,  700,  701,  702,  703,  704,  705,
          741,  742,  743,  744,  745,  746,  706,  707,  708,  709,
          739,  730,  728,  729,  725,  726,   46,  717,  723,  724,
          731,  732,  734,  733,  735,  736,   52,   53,  420,   54,
           55,  727,  738,  737,  447,   56,   57,  332,   58,-32766,
        -32766,-32766,-32766,-32766,-32766,-32766,-32766,-32766,    7,-32767,
        -32767,-32767,-32767,   50,  329, 1185,  518,  945,  946,  947,
          944,  943,  942,  937, 1211,  306, 1213, 1212,  763,  764,
          821,   59,   60,-32766,-32766,-32766,  808,   61, 1172,   62,
          291,  292,   63,   64,   65,   66,   67,   68,   69,   70,
          441,   24,  299,   71,  413,-32766,-32766,-32766,  116, 1087,
         1088,  749,  633, 1178,  213,  214,  215,  464,-32766,-32766,
        -32766,  822,  407, 1099,  321,-32766,  900,-32766,-32766,-32766,
        -32766,-32766,-32766, 1036,  200, -269,  428,   27,-32766,  419,
        -32766,-32766,-32766,-32766,-32766,  120,  491,  945,  946,  947,
          944,  943,  942,  297,  473,  474,  283,  623,  125,-32766,
          893,  894,  339,  477,  478,-32766, 1093, 1094, 1095, 1096,
         1090, 1091,  307,  492,-32766,    8,  425,  492, 1097, 1092,
          425,  121, -220,  869,  460,   39,  412,  332,  318,   18,
          319,  421, -122, -122, -122,   -4,  822,  463,   99,  100,
          101,  811,  301, 1036,   38,   19,  422, -122,  465, -122,
          466, -122,  467, -122,  102,  423, -122, -122, -122,   28,
           29,  468,  424,  624,   30,  469,  425,  812,   72,  348,
          923,  349,  350,  470,  471,-32766,-32766,-32766,  298,  472,
         1036,  809,  793,  840,  475,  476,-32767,-32767,-32767,-32767,
           94,   95,   96,   97,   98,-32766,  440,-32766,-32766,-32766,
        -32766, 1137,  213,  214,  215,  295,  421,  239,  824,  638,
         -122,  280,  463,  893,  894,  367,  811, 1036, 1203,   38,
           19,  422,  200,  465, 1054,  466,  492,  467,  127,  425,
          423,  213,  214,  215,   28,   29,  468,  424,  414,   30,
          469, 1036,  870,   72,  317,  822,  349,  350,  470,  471,
         1036,  200,  214,  215,  472, 1182,  919,  755,  840,  475,
          476,  213,  214,  215,  295,  918,   76,   77,   78,   47,
          338,  200,  477,  644,  326,  438,   31,  294,  331,  805,
          334,  200,  241,  824,  638,   -4,   32,  119,   79,   80,
           81,   82,   83,   84,   85,   86,   87,   88,   89,   90,
           91,   92,   93,   94,   95,   96,   97,   98,   99,  100,
          101, 1208,  301,  242,  822,  421,  801,  124,-32766,-32766,
        -32766,  463,  899,  207,  102,  811,  909,  126,   38,   19,
          422,  545,  465, 1172,  466,   34,  467,  762,-32766,  423,
        -32766,-32766,  647,   28,   29,  468,  822, 1036,   30,  469,
         -216,  117,   72,  803,   49,  349,  350,-32766,-32766,-32766,
        -32766,-32766,-32766,  472,  123, 1036,  234,  235,  236,  213,
          214,  215, 1036,  115,  641, 1138,  124,-32766,  200,-32766,
        -32766,-32766,  237,  238,  421,   96,   97,   98,  293,  200,
          463,  585,  856,  638,  811,  439, 1036,   38,   19,  422,
          284,  465,  215,  466,  749,  467, 1178,  339,  423,  231,
          232,  233,   28,   29,  468,  822,  421,   30,  469,  296,
          200,   72,  463,  415,  349,  350,  811,-32766,-32766,   38,
           19,  422,  472,  465,-32766,  466,  118,  467,  377, 1064,
          423,-32766,-32766,  642,   28,   29,  468,  822, 1099,   30,
          469,-32766,  434,   72,  129,  640,  349,  350,  576,  205,
          492,  824,  638,  425,  472,  206,-32766,-32766,-32766,  244,
          492,  237,  238,  425,  243,  653,  449,   20,  429,  301,
          332,  454,  591,  130,  357,  421,-32766,  763,  764,  599,
          600,  463,  646,  824,  638,  811,  922,  666,   38,   19,
          422,  308,  465,  650,  466,  128,  467,  756,  643,  423,
          820,  934,  656,   28,   29,  468,  822,  421,   30,  469,
          833,  102,   72,  463,   44,  349,  350,  811,   51,   48,
           38,   19,  422,  472,  465,   43,  466,   41,  467,  299,
           45,  423,   42,  605,  513,   28,   29,  468,-32766,  632,
           30,  469,  579,  432,   72,  749,  750,  349,  350,  534,
          512,  435,  824,  638, 1206,  472,  433,   33,  103,  104,
          105,  106,  107,  108,  109,  110,  111,  112,  113,  114,
          533,  776,  517,  524,  437,  622,  421, 1057,  612,  516,
          602,  619,  463,  279,  824,  638,  811,  458,  595,   38,
           19,  422,  596,  465,  330,  466,  240,  467,  975,  977,
          423,  609,  582,  -80,   28,   29,  468,  537,   12,   30,
          469,  477,  327,   72,  208,  209,  349,  350,    9, 1098,
          210,  303,  211,  333,  472,  842,  841,  384,  757,  370,
            0,  328,    0,    0,  202,  322,    0,    0, -497,  208,
          209,    0, 1087, 1088,  320,  210,-32766,  211, -498,    0,
         1089, 1144,    0,  824,  638,    0,    0,    4,  835,  202,
         -398, -407,    0,    3,   11, -406,   75, 1087, 1088,    0,
         -497,-32766,  409,  393,  408, 1089,  385,  434,  526,  372,
          302, 1143,  864,  863,  796,  857,  813,  798,  819,  807,
            0,  761,  661,  660,   37,   36,  926,  565,  810, 1093,
         1094, 1095, 1096, 1090, 1091,  383,  854,  852,  929,  804,
          759, 1097, 1092,  806,  818,  290,  760,  928,  212,  802,
        -32766,  930,  565,  927, 1093, 1094, 1095, 1096, 1090, 1091,
          383,  872, 1209,  639,  649,  651, 1097, 1092,  652,  654,
          655, 1034,  658,  212,  663,-32766,  664,  665,  122,  324,
          325,  405,  406,    0,  758, 1210,  839,  838,  766,  453,
         1207, 1179, 1177, 1163, 1175, 1078,  911, 1183, 1173,  829,
          836, 1038, 1039,  827,  935,  794,  765,  837,  662, 1050,
          861,  768,  767,  862,    0,  304,  289,  281,   25,   26,
          203,  305,  335,   74,   73,  411,  417,   35,   40,-32766,
           22,    0, 1015,  569, -217, 1016, 1103,  901, 1080, 1044,
         1040, 1041,  629,  559,  461,  457,  455,  450,  378,   16,
           15,   14, -216,    0,    0, -416,    0, 1045,  603, 1157,
         1104, 1205, 1077, 1174, 1158, 1162, 1176, 1063, 1048, 1049,
         1046, 1047
    );

    protected $actionCheck = array(
            2,    3,    4,    5,    6,    8,    8,    9,   10,   11,
           12,   31,   32,   33,   34,   35,   36,   37,   38,   39,
            7,   41,   42,   43,   44,   45,   46,   47,   48,   49,
           50,   51,   52,    8,    9,   10,   31,   32,   33,   34,
           35,   36,   37,   38,   39,    7,   66,   67,   31,   32,
           33,   34,   54,   28,    0,   30,   31,   32,   33,   34,
           35,   36,   37,   38,   39,   40,   68,   69,   70,   71,
           72,   73,   74,   79,    7,   77,   31,   32,   33,   34,
           35,    7,   84,   85,   86,   87,   88,   89,   90,   91,
           92,   93,   94,   95,   96,   97,   98,   99,  100,  101,
          102,  103,  104,  105,  106,  107,  108,  109,  110,  111,
          112,  113,  114,  115,  116,  117,  118,  119,  120,  121,
          122,  123,  124,  125,  126,  127,   67,  129,  130,  131,
          132,  133,  134,  135,  136,  137,    2,    3,    4,    5,
            6,  143,  144,  145,    7,   11,   12,  153,   14,   31,
           32,   33,   34,   35,   36,   37,   38,   39,  103,   41,
           42,   43,   44,   67,  109,  152,   82,  112,  113,  114,
          115,  116,  117,  118,   77,    7,   79,   80,  102,  103,
            1,   47,   48,    8,    9,   10,  148,   53,   79,   55,
           56,   57,   58,   59,   60,   61,   62,   63,   64,   65,
            7,   67,   68,   69,   70,    8,    9,   10,  149,   75,
           76,   77,   77,   79,    8,    9,   10,   83,    8,    9,
           10,    1,  146,  139,  128,   28,  152,   30,   31,   32,
           33,   34,   35,   12,   28,   79,  102,    7,   28,    7,
           30,   31,   32,   33,   34,  149,  112,  112,  113,  114,
          115,  116,  117,    7,  120,  121,   35,   77,  149,  103,
          130,  131,  153,  129,  130,  109,  132,  133,  134,  135,
          136,  137,  138,  143,  118,    7,  146,  143,  144,  145,
          146,    7,  152,   29,    7,  151,    7,  153,  154,  152,
          156,   71,   72,   73,   74,    0,    1,   77,   50,   51,
           52,   81,   54,   12,   84,   85,   86,   87,   88,   89,
           90,   91,   92,   93,   66,   95,   96,   97,   98,   99,
          100,  101,  102,  143,  104,  105,  146,  148,  108,    7,
          150,  111,  112,  113,  114,    8,    9,   10,   35,  119,
           12,  148,  122,  123,  124,  125,   41,   42,   43,   44,
           45,   46,   47,   48,   49,   28,    7,   30,   31,   32,
           33,  155,    8,    9,   10,   35,   71,   13,  148,  149,
          150,   13,   77,  130,  131,   79,   81,   12,   82,   84,
           85,   86,   28,   88,  152,   90,  143,   92,   67,  146,
           95,    8,    9,   10,   99,  100,  101,  102,  103,  104,
          105,   12,  148,  108,  109,    1,  111,  112,  113,  114,
           12,   28,    9,   10,  119,   77,  148,  122,  123,  124,
          125,    8,    9,   10,   35,  148,    8,    9,   10,   67,
           67,   28,  129,   29,    7,   29,  140,  141,  143,  148,
            7,   28,   29,  148,  149,  150,   28,   13,   30,   31,
           32,   33,   34,   35,   36,   37,   38,   39,   40,   41,
           42,   43,   44,   45,   46,   47,   48,   49,   50,   51,
           52,  150,   54,   15,    1,   71,  148,  147,    8,    9,
           10,   77,  152,   15,   66,   81,   79,  149,   84,   85,
           86,  128,   88,   79,   90,   13,   92,  148,   28,   95,
           30,   31,   29,   99,  100,  101,    1,   12,  104,  105,
          152,  149,  108,  148,   67,  111,  112,    8,    9,   10,
           31,   32,   33,  119,   29,   12,   50,   51,   52,    8,
            9,   10,   12,   15,   29,  152,  147,   28,   28,   30,
           31,   32,   66,   67,   71,   47,   48,   49,   35,   28,
           77,   82,  148,  149,   81,  149,   12,   84,   85,   86,
          153,   88,   10,   90,   77,   92,   79,  153,   95,   47,
           48,   49,   99,  100,  101,    1,   71,  104,  105,   35,
           28,  108,   77,  123,  111,  112,   81,    8,    9,   84,
           85,   86,  119,   88,   31,   90,  149,   92,   78,  112,
           95,   31,   32,   29,   99,  100,  101,    1,  139,  104,
          105,  151,  146,  108,  149,  149,  111,  112,  153,   15,
          143,  148,  149,  146,  119,   15,    8,    9,   10,   15,
          143,   66,   67,  146,   15,   29,   72,   73,  151,   54,
          153,   72,   73,   97,   98,   71,   28,  102,  103,  106,
          107,   77,   29,  148,  149,   81,  148,  149,   84,   85,
           86,   29,   88,   29,   90,   29,   92,  148,  149,   95,
           29,  148,  149,   99,  100,  101,    1,   71,  104,  105,
           35,   66,  108,   77,   67,  111,  112,   81,   67,   67,
           84,   85,   86,  119,   88,   67,   90,   67,   92,   68,
           67,   95,   67,   74,   77,   99,  100,  101,   82,   89,
          104,  105,   87,  102,  108,   77,   77,  111,  112,   77,
           77,   77,  148,  149,   77,  119,   77,   15,   16,   17,
           18,   19,   20,   21,   22,   23,   24,   25,   26,   27,
           77,   77,   77,   82,   86,   79,   71,   79,   79,   79,
           79,   91,   77,   94,  148,  149,   81,  102,   96,   84,
           85,   86,  109,   88,  110,   90,   29,   92,   56,   57,
           95,   93,   96,   94,   99,  100,  101,   94,   94,  104,
          105,  129,  126,  108,   47,   48,  111,  112,  142,  139,
           53,  151,   55,  126,  119,  123,  123,  146,  150,  142,
           -1,  127,   -1,   -1,   67,  128,   -1,   -1,  128,   47,
           48,   -1,   75,   76,  128,   53,   79,   55,  128,   -1,
           83,  139,   -1,  148,  149,   -1,   -1,  142,  147,   67,
          142,  142,   -1,  142,  142,  142,  149,   75,   76,   -1,
          128,   79,  146,  146,  146,   83,  146,  146,  146,  146,
          151,  156,  148,  148,  148,  148,  148,  148,  148,  148,
           -1,  148,  148,  148,  148,  148,  148,  130,  148,  132,
          133,  134,  135,  136,  137,  138,  148,  148,  148,  148,
          148,  144,  145,  148,  148,  151,  148,  148,  151,  148,
          153,  148,  130,  148,  132,  133,  134,  135,  136,  137,
          138,  148,  150,  149,  149,  149,  144,  145,  149,  149,
          149,  154,  149,  151,  149,  153,  149,  149,  149,  149,
          149,  149,  149,   -1,  150,  150,  150,  150,  150,  150,
          150,  150,  150,  150,  150,  150,  150,  150,  150,  150,
          150,  150,  150,  150,  150,  150,  150,  150,  150,  150,
          150,  150,  150,  150,   -1,  151,  151,  151,  151,  151,
          151,  151,  151,  151,  151,  151,  151,  151,  151,  151,
          151,   -1,  152,  152,  152,  152,  152,  152,  152,  152,
          152,  152,  152,  152,  152,  152,  152,  152,  152,  152,
          152,  152,  152,   -1,   -1,  154,   -1,  155,  155,  155,
          155,  155,  155,  155,  155,  155,  155,  155,  155,  155,
          155,  155
    );

    protected $actionBase = array(
            0,  220,  295,  109,  109,  180,  739,   -2,   -2,   -2,
           -2,   -2,  135,  574,  473,  606,  473,  505,  404,  675,
          675,  675,  330,  389,  513,  513,  826,  513,  328,  365,
          291,  520,  495,  221,  544,  398,  398,  398,  398,  134,
          134,  398,  398,  398,  398,  398,  398,  398,  398,  398,
          398,  398,  398,  398,  398,  398,  398,  398,  398,  398,
          398,  398,  398,  398,  398,  398,  398,  398,  398,  398,
          398,  398,  398,  398,  398,  398,  398,  398,  398,  398,
          398,  398,  398,  398,  398,  398,  398,  398,  398,  398,
          398,  398,  398,  398,  398,  398,  398,  398,  398,  398,
          398,  398,  398,  398,  398,  398,  398,  398,  398,  398,
          398,  398,  398,  398,  398,  398,  398,  398,  398,  398,
          398,  398,  398,  398,  398,  398,  398,  398,  398,  398,
          398,  254,  179,  434,  482,  741,  731,  735,  736,  828,
          659,  823,  780,  781,  636,  782,  783,  784,  785,  786,
          779,  787,  843,  788,  418,  418,  418,  418,  418,  418,
          418,  418,  418,  418,  418,   -3,  354,  383,  413,  206,
          579,  521,  521,  521,  521,  521,  521,  521,  175,  175,
          175,  175,  175,  175,  175,  175,  175,  175,  175,  175,
          175,  175,  175,  175,  175,  403,  618,  618,  618,  552,
          737,  510,  762,  762,  762,  762,  762,  762,  762,  762,
          762,  762,  762,  762,  762,  762,  762,  762,  762,  762,
          762,  762,  762,  762,  762,  762,  762,  762,  762,  762,
          762,  762,  762,  762,  762,  762,  762,  762,  762,  762,
          762,  762,  762,  762,  762,  470,  -20,  -20,  509,  563,
          327,  570,  210,  489,  197,   25,   25,   25,   25,   25,
           17,   45,    5,    5,    5,    5,  712,  305,  305,  305,
          305,  118,  118,  118,  118,  776,  777,  797,  799,  303,
          303,  652,  652,  631,  769,  498,  498,  522,  522,  487,
          487,  487,  487,  487,  487,  487,  487,  487,  487,  460,
          156,  818,  130,  130,  130,  130,  243,   84,  243,  682,
          695,  248,  248,  248,  476,  476,  476,   76,  661,  296,
          338,  338,  338,  296,  545,  545,  545,  477,  477,  477,
          477,  466,  687,  477,  477,  477,  362,  626,   97,  465,
          676,  800,  662,  803,  508,  689,   96,  698,  696,  407,
          611,  564,  569,  543,  688,  406,  407,  254,  523,  447,
          585,  720,  642,  349,  732,   38,  193,  363,  519,   59,
          414,  137,  770,  738,  821,  820,   13,  321,  690,  585,
          585,  585,   74,  469,  771,  772,   59,  358,  565,  565,
          565,  565,  802,  773,  565,  565,  565,  565,  801,  796,
          268,  277,  778,  232,  718,  638,  638,  638,  638,  638,
          638,  645,  638,  808,  627,  819,  819,  663,  671,  645,
          817,  817,  817,  817,  645,  638,  819,  819,  645,  631,
          819,  230,  645,  656,  638,  667,  667,  817,  715,  714,
          627,  670,  674,  819,  819,  819,  674,  663,  645,  817,
          653,  681,   67,  819,  817,  632,  632,  653,  645,  632,
          671,  632,   54,  641,  630,  816,  813,  815,  643,  754,
          673,  672,  805,  734,  812,  665,  649,  806,  807,  702,
          713,  711,  644,  518,  635,  628,  617,  633,  691,  622,
          686,  611,  701,  615,  615,  615,  680,  685,  680,  615,
          615,  615,  615,  615,  615,  615,  615,  842,  657,  693,
          677,  658,  710,  604,  703,  683,  610,  763,  650,  702,
          702,  795,  829,  836,  841,  757,  639,  699,  831,  680,
          856,  717,  274,  468,  640,  798,  651,  664,  700,  680,
          804,  680,  765,  680,  827,  647,  775,  702,  774,  615,
          825,  855,  854,  853,  852,  851,  850,  849,  848,  621,
          847,  709,  625,  835,  168,  809,  688,  646,  697,  708,
          433,  846,  648,  680,  680,  767,  687,  680,  768,  753,
          716,  839,  705,  834,  845,  650,  833,  680,  655,  844,
          433,  623,  629,  822,  678,  704,  814,  669,  824,  811,
          755,  458,  619,  752,  634,  706,  838,  837,  840,  707,
          756,  759,  614,  660,  668,  666,  789,  760,  810,  728,
          790,  791,  830,  679,  701,  692,  654,  684,  620,  761,
          792,  832,  729,  730,  743,  793,  745,  794,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,  134,  134,
           -2,   -2,   -2,   -2,    0,    0,    0,    0,    0,   -2,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,    0,    0,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  418,  418,
          418,  418,  418,  418,  418,  418,  418,  418,  418,  418,
          418,  418,  418,  418,  418,  418,  418,  418,  418,  418,
          418,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,  418,  -20,  -20,  -20,  -20,  418,  -20,  -20,
          -20,  -20,  -20,  -20,  -20,  418,  418,  418,  418,  418,
          418,  418,  418,  418,  418,  418,  418,  418,  418,  418,
          418,  418,  -20,  418,  418,  418,  -20,  487,  -20,  487,
          487,  487,  487,  487,  487,  487,  487,  487,  487,  487,
          487,  487,  487,  487,  487,  487,  487,  487,  487,  487,
          487,  487,  487,  487,  487,  487,  487,  487,  487,  487,
          487,  487,  487,  487,  487,  487,  487,  487,  487,  487,
          487,  487,  418,    0,    0,  418,  -20,  418,  -20,  418,
          -20,  418,  418,  418,  418,  418,  418,  -20,  -20,  -20,
          -20,  -20,  -20,    0,  248,  248,  248,  248,  -20,  -20,
          -20,  -20,   55,   55,   55,   55,  487,  487,  487,  487,
          487,  487,  248,  248,  476,  476,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,  487,   55,  487,  638,
          638,  638,  638,  638,  296,  638,  296,  296,    0,    0,
            0,    0,    0,    0,  638,  296,    0,   -6,   -6,   -6,
            0,  638,  638,  638,  638,  638,  638,  638,  638,   -6,
          638,  638,  638,  819,  296,    0,   -6,  546,  546,  546,
          546,  433,   59,    0,  638,  638,    0,  670,    0,    0,
            0,  819,    0,    0,    0,    0,    0,  615,  274,  699,
            0,  322,    0,    0,    0,    0,    0,    0,    0,  639,
          322,  246,  246,    0,    0,  621,  615,  615,  615,    0,
            0,  639,  639,    0,    0,    0,    0,    0,    0,  427,
          639,    0,    0,    0,    0,  427,  279,    0,    0,  279,
            0,  433
    );

    protected $actionDefault = array(
            3,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,  524,  524,32767,  481,32767,32767,
        32767,32767,32767,32767,32767,  287,  287,  287,32767,32767,
        32767,  513,  513,  513,  513,  513,  513,  513,  513,  513,
          513,  513,32767,32767,32767,32767,32767,  369,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,  375,  529,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,  350,  351,  353,  354,  286,  514,
          237,  376,  528,  285,  239,  314,  485,32767,32767,32767,
          316,  116,  248,  193,  484,  119,  284,  224,  368,  370,
          315,  291,  296,  297,  298,  299,  300,  301,  302,  303,
          304,  305,  306,  307,  290,  441,  347,  346,  345,  443,
        32767,  442,  478,  478,  481,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,  312,  469,  468,  313,  439,
          317,  440,  319,  444,  318,  335,  336,  333,  334,  337,
          446,  445,  462,  463,  460,  461,  289,  338,  339,  340,
          341,  464,  465,  466,  467,  271,  271,  271,  271,32767,
        32767,  523,  523,32767,32767,  326,  327,  453,  454,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
          272,32767,  228,  228,  228,  228,  228,32767,32767,32767,
        32767,  321,  322,  320,  448,  449,  447,32767,  415,32767,
        32767,32767,32767,  417,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,  486,32767,32767,32767,
        32767,32767,32767,32767,32767,  499,  404,32767,32767,32767,
          397,  212,  214,  161,  472,32767,32767,32767,32767,  504,
          331,32767,32767,32767,32767,32767,32767,  537,32767,  499,
        32767,32767,32767,32767,32767,32767,32767,32767,  344,  323,
          324,  325,32767,32767,32767,32767,  503,  497,  456,  457,
          458,  459,32767,32767,  450,  451,  452,  455,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,  165,32767,  412,32767,  418,  418,32767,32767,  165,
        32767,32767,32767,32767,  165,32767,  502,  501,  165,32767,
          398,  480,  165,  178,32767,  176,  176,32767,  198,  198,
        32767,32767,  180,  473,  492,32767,  180,32767,  165,32767,
          386,  167,  480,32767,32767,  230,  230,  386,  165,  230,
        32767,  230,32767,   82,  422,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,  399,
        32767,32767,32767,32767,  365,  366,  475,  488,32767,  489,
        32767,  397,32767,  329,  330,  332,  309,32767,  311,  355,
          356,  357,  358,  359,  360,  361,  363,32767,32767,  402,
          405,32767,32767,32767,   84,  108,  247,32767,  536,   84,
          400,32767,32767,  294,  536,32767,32767,32767,32767,  531,
        32767,32767,  288,32767,32767,32767,   84,32767,   84,  243,
        32767,  163,32767,  521,32767,32767,  497,  401,32767,  328,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,  498,
        32767,32767,32767,32767,  219,32767,  435,32767,   84,32767,
          179,32767,32767,  292,  238,32767,32767,  530,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,  164,32767,32767,
          181,32767,32767,  497,32767,32767,32767,32767,32767,32767,
        32767,32767,  283,32767,32767,32767,32767,32767,  497,32767,
        32767,32767,  223,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,   82,   60,32767,  265,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,  121,  121,
            3,  121,  121,    3,  121,  121,  121,  121,  121,  121,
          121,  121,  121,  121,  121,  121,  121,  206,  250,  209,
          198,  198,  158,  250,  250,  250,  257
    );

    protected $goto = array(
          160,  160,  134,  134,  139,  134,  135,  136,  137,  142,
          144,  181,  162,  158,  158,  158,  158,  139,  139,  159,
          159,  159,  159,  159,  159,  159,  159,  159,  159,  159,
          154,  155,  156,  157,  178,  133,  179,  493,  494,  360,
          495,  499,  500,  501,  502,  503,  504,  505,  506,  962,
          138,  140,  141,  143,  165,  170,  180,  196,  245,  248,
          250,  252,  254,  255,  256,  257,  258,  259,  267,  268,
          269,  270,  285,  286,  311,  312,  313,  379,  380,  381,
          549,  182,  183,  184,  185,  186,  187,  188,  189,  190,
          191,  192,  193,  194,  145,  146,  147,  161,  148,  163,
          149,  197,  164,  150,  151,  152,  198,  153,  131,  625,
          567,  753,  567,  567,  567,  567,  567,  567,  567,  567,
          567,  567,  567,  567,  567,  567,  567,  567,  567,  567,
          567,  567,  567,  567,  567,  567,  567,  567,  567,  567,
          567,  567,  567,  567,  567,  567,  567,  567,  567,  567,
          567,  567,  567,  567,  567, 1100,    6, 1100, 1100, 1100,
         1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100,
         1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100,
         1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100,
         1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100, 1100,
          885,  885, 1189, 1189,  583,  586,  631,  168,  341,  509,
          754,  509,  171,  172,  173,  388,  389,  390,  391,  167,
          195,  199,  201,  249,  251,  253,  260,  261,  262,  263,
          264,  265,  271,  272,  273,  274,  287,  288,  314,  315,
          316,  394,  395,  396,  397,  169,  174,  246,  247,  175,
          176,  177,  497,  497,  497,  497,  497,  497,  523, 1200,
         1200,  784,  497,  497,  497,  497,  497,  497,  497,  497,
          497,  497,  508, 1200,  508,  387,  608,  543,  543,  573,
          539,  580,  606,  790,  752,  541,  541,  496,  498,  529,
          546,  574,  577,  587,  593,  871, 1169,  851, 1169,  657,
          634,  511,  880,  875,  566,  815,  566,  566,  566,  566,
          566,  566,  566,  566,  566,  566,  566,  566,  566,  566,
          566,  566,  566,  566,  566,  566,  566,  566,  566,  566,
          566,  566,  566,  566,  566,  566,  566,  566,  566,  566,
          566,  566,  566,  566,  566,  566,  566,  566,  566,  551,
          552,  553,  554,  555,  556,  557,  558,  560,  589,  514,
          855,  550,  590,  344,  404,  522,  519,  519,  519,  443,
          445,  933,  636,  519, 1161, 1101,  618,  931,  522,  522,
          276,  277,  278,  430,  430,  430,  430,  430,  430,  538,
          519,  903, 1058,  430,  430,  430,  430,  430,  430,  430,
          430,  430,  430, 1065,  544, 1065,  892,  892,  892,  892,
          892,  535,  892,  659,  562,  777,  594,  868,  882,  613,
          867,  616,  878,  620,  621,  628,  630,  635,  637,  342,
          343,  849,  849,  849,  849,  323,  310,  844,  850,  615,
          548, 1199, 1199,  572,  941,  777,  777,  519,  519,  536,
          568,  519,  519, 1081,  519, 1199,  510, 1168,  510, 1168,
         1019,   17,   13,  355, 1061, 1062, 1193,  520, 1058, 1202,
          611, 1076, 1075,  617,  361,  358,  547,  561, 1184, 1184,
         1184, 1059, 1160, 1059,  598,  607, 1186, 1149,  362,   21,
         1167, 1060,  527,  375,  604, 1009,  540,  369,  369,  369,
          898,  889,  960,  770,  770,  778,  778,  778,  780,  369,
          769,  398,  451,  347,  773,  368,  386,  373,  907,  771,
          645,  402,   10, 1051, 1056,  446,  781,  578,  912,  859,
         1146,  459,  949,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,  528
    );

    protected $gotoCheck = array(
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   53,
          112,   11,  112,  112,  112,  112,  112,  112,  112,  112,
          112,  112,  112,  112,  112,  112,  112,  112,  112,  112,
          112,  112,  112,  112,  112,  112,  112,  112,  112,  112,
          112,  112,  112,  112,  112,  112,  112,  112,  112,  112,
          112,  112,  112,  112,  112,  119,   90,  119,  119,  119,
          119,  119,  119,  119,  119,  119,  119,  119,  119,  119,
          119,  119,  119,  119,  119,  119,  119,  119,  119,  119,
          119,  119,  119,  119,  119,  119,  119,  119,  119,  119,
          119,  119,  119,  119,  119,  119,  119,  119,  119,  119,
           70,   70,   70,   70,   56,   56,   56,   23,   65,  112,
           12,  112,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,  109,  109,  109,  109,  109,  109,   93,  134,
          134,   25,  109,  109,  109,  109,  109,  109,  109,  109,
          109,  109,  109,  134,  109,   47,   47,   47,   47,   47,
           47,   36,   36,   10,   10,   47,   47,   47,   47,   47,
           47,   47,   47,   47,   47,   10,  110,   10,  110,   10,
            5,   10,   10,   10,   53,   46,   53,   53,   53,   53,
           53,   53,   53,   53,   53,   53,   53,   53,   53,   53,
           53,   53,   53,   53,   53,   53,   53,   53,   53,   53,
           53,   53,   53,   53,   53,   53,   53,   53,   53,   53,
           53,   53,   53,   53,   53,   53,   53,   53,   53,  102,
          102,  102,  102,  102,  102,  102,  102,  102,  102,    8,
           29,   40,   63,   63,   63,   40,    8,    8,    8,    7,
            7,    7,    7,    8,   75,    7,    7,    7,   40,   40,
           61,   61,   61,   53,   53,   53,   53,   53,   53,    8,
            8,   77,   75,   53,   53,   53,   53,   53,   53,   53,
           53,   53,   53,   53,  101,   53,   53,   53,   53,   53,
           53,   28,   53,   28,   28,   19,   28,   28,   28,   28,
           28,   28,   28,   28,   28,   28,   28,   28,   28,   65,
           65,   53,   53,   53,   53,  118,  118,   53,   53,   53,
            2,  133,  133,    2,   90,   19,   19,    8,    8,    8,
            8,    8,    8,   30,    8,  133,  115,  111,  115,  111,
           30,   30,   30,   30,   75,   75,  132,    8,   75,  133,
           57,  117,  117,   57,   43,   57,    8,   30,  111,  111,
          111,   75,   75,   75,  120,   45,  130,  124,   54,   30,
          111,   75,   54,   44,   30,   94,   54,  116,  116,  116,
           74,   72,   93,   19,   19,   19,   19,   19,   19,  116,
           19,   18,   54,   14,   21,    9,  116,   13,   78,   20,
           67,   17,   54,  105,  107,   59,   22,   60,   79,   64,
          123,  100,   92,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   93
    );

    protected $gotoBase = array(
            0,    0, -200,    0,    0,  288,    0,  366,   42,  184,
          282,  109,  208,  170,  196,    0,    0,  115,  186,   98,
          171,  188,   86,    7,    0,  253,    0,    0, -228,  342,
           40,    0,    0,    0,    0,    0,  245,    0,    0,  -22,
          339,    0,    0,  436,  203,  205,  289,   -4,    0,    0,
            0,    0,    0,  104,   64,    0,  -99,   14,    0,   89,
           81, -283,    0,   34,   82, -231,    0,  163,    0,    0,
          -79,    0,  195,    0,  192,   38,    0,  368,  162,   87,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
          144,    0,   72,  219,  194,    0,    0,    0,    0,    0,
           74,  379,  307,    0,    0,  107,    0,  105,    0,  -27,
           -3,  158,  -90,    0,    0,  157,  177,  150,  117,  -45,
          281,    0,    0,   78,  283,    0,    0,    0,    0,    0,
          204,    0,  439,  132,  -50,    0
    );

    protected $gotoDefault = array(
        -32768,  462,  668,    2,  669,  740,  748,  601,  479,  515,
          853,  791,  792,  364,  410,  480,  363,  399,  392,  779,
          772,  774,  782,  166,  400,  785,    1,  787,  521,  823,
         1010,  351,  795,  352,  592,  797,  531,  799,  800,  132,
          481,  365,  366,  532,  374,  581,  814,  266,  371,  816,
          353,  817,  826,  354,  614,  597,  563,  610,  482,  442,
          575,  275,  542,  570,  858,  340,  866,  648,  874,  877,
          483,  564,  888,  448,  896, 1086,  382,  902,  908,  913,
          916,  418,  401,  588,  920,  921,    5,  925,  626,  627,
          940,  300,  948,  961,  416, 1029, 1031,  484,  485,  525,
          456,  507,  530,  486, 1052,  436,  403, 1055,  487,  488,
          426,  427, 1073, 1070,  346, 1154,  345,  444,  309, 1141,
          584, 1105,  452, 1192, 1150,  336,  489,  490,  359,  376,
         1187,  431, 1194, 1201,  337,  571
    );

    protected $ruleToNonTerminal = array(
            0,    1,    3,    3,    2,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    6,    6,    6,    6,    6,    6,    6,
            7,    7,    8,    8,    9,    4,    4,    4,    4,    4,
            4,    4,    4,    4,    4,    4,   14,   14,   15,   15,
           15,   15,   17,   17,   13,   13,   18,   18,   19,   19,
           20,   20,   21,   21,   16,   16,   22,   24,   24,   25,
           26,   26,   28,   27,   27,   27,   27,   29,   29,   29,
           29,   29,   29,   29,   29,   29,   29,   29,   29,   29,
           29,   29,   29,   29,   29,   29,   29,   29,   29,   29,
           29,   29,   29,   29,   29,   29,   10,   10,   50,   50,
           52,   51,   51,   44,   44,   54,   54,   55,   55,   11,
           12,   12,   12,   58,   58,   58,   59,   59,   62,   62,
           60,   60,   63,   63,   37,   37,   46,   46,   49,   49,
           49,   48,   48,   64,   38,   38,   38,   38,   65,   65,
           66,   66,   67,   67,   35,   35,   31,   31,   68,   33,
           33,   69,   32,   32,   34,   34,   45,   45,   45,   56,
           56,   71,   71,   72,   72,   74,   74,   74,   73,   73,
           57,   57,   75,   75,   75,   76,   76,   77,   77,   77,
           41,   41,   78,   78,   78,   42,   42,   79,   79,   61,
           61,   80,   80,   80,   80,   85,   85,   86,   86,   87,
           87,   87,   87,   87,   88,   89,   89,   84,   84,   81,
           81,   83,   83,   91,   91,   90,   90,   90,   90,   90,
           90,   82,   82,   92,   92,   43,   43,   36,   36,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   39,   39,   39,   39,   39,   39,   39,
           39,   39,   39,   30,   30,   40,   40,   97,   97,   98,
           98,   98,   98,  104,   93,   93,  100,  100,  106,  106,
          107,  108,  108,  108,  108,  108,  108,  112,  112,   53,
           53,   53,   94,   94,  113,  113,  109,  109,  114,  114,
          114,  114,   95,   95,   95,   99,   99,   99,  105,  105,
          119,  119,  119,  119,  119,  119,  119,  119,  119,  119,
          119,  119,  119,   23,   23,   23,   23,   23,   23,  121,
          121,  121,  121,  121,  121,  121,  121,  121,  121,  121,
          121,  121,  121,  121,  121,  121,  121,  121,  121,  121,
          121,  121,  121,  121,  121,  121,  121,  121,  121,  121,
          121,  121,  103,  103,   96,   96,   96,   96,  120,  120,
          123,  123,  122,  122,  124,  124,   47,   47,   47,   47,
          126,  126,  125,  125,  125,  125,  125,  127,  127,  111,
          111,  115,  115,  110,  110,  128,  128,  128,  128,  116,
          116,  116,  116,  102,  102,  117,  117,  117,   70,  129,
          129,  130,  130,  130,  101,  101,  131,  131,  132,  132,
          132,  132,  118,  118,  118,  118,  134,  133,  133,  133,
          133,  133,  133,  133,  135,  135,  135
    );

    protected $ruleToLength = array(
            1,    1,    2,    0,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    3,    1,    1,    1,    1,    1,    3,
            5,    4,    3,    4,    2,    3,    1,    1,    7,    8,
            6,    7,    3,    1,    3,    1,    3,    1,    1,    3,
            1,    2,    1,    2,    3,    1,    3,    3,    1,    3,
            2,    0,    1,    1,    1,    1,    1,    3,    5,    8,
            3,    5,    9,    3,    2,    3,    2,    3,    2,    3,
            2,    3,    3,    3,    1,    2,    5,    7,    9,    5,
            6,    3,    3,    2,    2,    1,    1,    1,    0,    2,
            8,    0,    4,    1,    3,    0,    1,    0,    1,   10,
            7,    6,    5,    1,    2,    2,    0,    2,    0,    2,
            0,    2,    1,    3,    1,    4,    1,    4,    1,    1,
            4,    1,    3,    3,    3,    4,    4,    5,    0,    2,
            4,    3,    1,    1,    1,    4,    0,    2,    3,    0,
            2,    4,    0,    2,    0,    3,    1,    2,    1,    1,
            0,    1,    3,    4,    6,    1,    1,    1,    0,    1,
            0,    2,    2,    3,    3,    1,    3,    1,    2,    2,
            3,    1,    1,    2,    4,    3,    1,    1,    3,    2,
            0,    3,    3,    9,    3,    1,    3,    0,    2,    4,
            5,    4,    4,    4,    3,    1,    1,    1,    3,    1,
            1,    0,    1,    1,    2,    1,    1,    1,    1,    1,
            1,    1,    3,    1,    3,    3,    1,    0,    1,    1,
            3,    3,    4,    4,    1,    2,    3,    3,    3,    3,
            3,    3,    3,    3,    3,    3,    3,    3,    2,    2,
            2,    2,    3,    3,    3,    3,    3,    3,    3,    3,
            3,    3,    3,    3,    3,    3,    3,    3,    3,    2,
            2,    2,    2,    3,    3,    3,    3,    3,    3,    3,
            3,    3,    3,    1,    3,    5,    4,    3,    4,    4,
            2,    2,    2,    2,    2,    2,    2,    2,    2,    2,
            2,    2,    2,    2,    1,    1,    1,    3,    2,    1,
            2,   10,   11,    3,    3,    2,    4,    4,    3,    4,
            4,    4,    4,    7,    3,    2,    0,    4,    1,    3,
            2,    2,    4,    6,    2,    2,    4,    1,    1,    1,
            2,    3,    1,    1,    1,    1,    1,    1,    3,    3,
            4,    4,    0,    2,    1,    0,    1,    1,    0,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    3,    2,    1,    3,    1,    4,    3,    1,    3,
            3,    3,    3,    3,    3,    3,    3,    3,    3,    3,
            3,    3,    3,    3,    3,    3,    2,    2,    2,    2,
            3,    3,    3,    3,    3,    3,    3,    3,    5,    4,
            4,    3,    1,    3,    1,    1,    3,    3,    0,    2,
            0,    1,    3,    1,    3,    1,    1,    1,    1,    1,
            6,    4,    3,    4,    2,    4,    4,    1,    3,    1,
            2,    1,    1,    4,    1,    3,    6,    4,    4,    4,
            4,    1,    4,    0,    1,    1,    3,    1,    4,    3,
            1,    1,    1,    0,    0,    2,    3,    1,    3,    1,
            4,    2,    2,    2,    1,    2,    1,    1,    4,    3,
            3,    3,    6,    3,    1,    1,    1
    );

    protected function reduceRule0() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule1() {
         $this->semValue = $this->handleNamespaces($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule2() {
         if (is_array($this->semStack[$this->stackPos-(2-2)])) { $this->semValue = array_merge($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)]); } else { $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)]; };
    }

    protected function reduceRule3() {
         $this->semValue = array();
    }

    protected function reduceRule4() {
         $startAttributes = $this->lookaheadStartAttributes; if (isset($startAttributes['comments'])) { $nop = new Stmt\Nop(['comments' => $startAttributes['comments']]); } else { $nop = null; };
            if ($nop !== null) { $this->semStack[$this->stackPos-(1-1)][] = $nop; } $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule5() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule6() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule7() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule8() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule9() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule10() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule11() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule12() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule13() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule14() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule15() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule16() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule17() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule18() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule19() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule20() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule21() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule22() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule23() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule24() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule25() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule26() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule27() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule28() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule29() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule30() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule31() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule32() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule33() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule34() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule35() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule36() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule37() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule38() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule39() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule40() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule41() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule42() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule43() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule44() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule45() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule46() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule47() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule48() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule49() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule50() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule51() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule52() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule53() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule54() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule55() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule56() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule57() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule58() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule59() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule60() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule61() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule62() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule63() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule64() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule65() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule66() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule67() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule68() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule69() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule70() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule71() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule72() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule73() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule74() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule75() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule76() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule77() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule78() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule79() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule80() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule81() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule82() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule83() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule84() {
         $this->semValue = new Name($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule85() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule86() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule87() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule88() {
         $this->semValue = new Stmt\HaltCompiler($this->lexer->handleHaltCompiler(), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule89() {
         $this->semValue = new Stmt\Namespace_($this->semStack[$this->stackPos-(3-2)], null, $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule90() {
         $this->semValue = new Stmt\Namespace_($this->semStack[$this->stackPos-(5-2)], $this->semStack[$this->stackPos-(5-4)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule91() {
         $this->semValue = new Stmt\Namespace_(null, $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule92() {
         $this->semValue = new Stmt\Use_($this->semStack[$this->stackPos-(3-2)], Stmt\Use_::TYPE_NORMAL, $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule93() {
         $this->semValue = new Stmt\Use_($this->semStack[$this->stackPos-(4-3)], $this->semStack[$this->stackPos-(4-2)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule94() {
         $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule95() {
         $this->semValue = new Stmt\Const_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule96() {
         $this->semValue = Stmt\Use_::TYPE_FUNCTION;
    }

    protected function reduceRule97() {
         $this->semValue = Stmt\Use_::TYPE_CONSTANT;
    }

    protected function reduceRule98() {
         $this->semValue = new Stmt\GroupUse(new Name($this->semStack[$this->stackPos-(7-3)], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(7-6)], $this->semStack[$this->stackPos-(7-2)], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes);
    }

    protected function reduceRule99() {
         $this->semValue = new Stmt\GroupUse(new Name($this->semStack[$this->stackPos-(8-4)], $this->startAttributeStack[$this->stackPos-(8-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(8-7)], $this->semStack[$this->stackPos-(8-2)], $this->startAttributeStack[$this->stackPos-(8-1)] + $this->endAttributes);
    }

    protected function reduceRule100() {
         $this->semValue = new Stmt\GroupUse(new Name($this->semStack[$this->stackPos-(6-2)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(6-5)], Stmt\Use_::TYPE_UNKNOWN, $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule101() {
         $this->semValue = new Stmt\GroupUse(new Name($this->semStack[$this->stackPos-(7-3)], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(7-6)], Stmt\Use_::TYPE_UNKNOWN, $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes);
    }

    protected function reduceRule102() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule103() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule104() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule105() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule106() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule107() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule108() {
         $this->semValue = new Stmt\UseUse($this->semStack[$this->stackPos-(1-1)], null, Stmt\Use_::TYPE_UNKNOWN, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule109() {
         $this->semValue = new Stmt\UseUse($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], Stmt\Use_::TYPE_UNKNOWN, $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule110() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule111() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule112() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)]; $this->semValue->type = Stmt\Use_::TYPE_NORMAL;
    }

    protected function reduceRule113() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)]; $this->semValue->type = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule114() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule115() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule116() {
         $this->semValue = new Node\Const_($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule117() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule118() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule119() {
         $this->semValue = new Node\Const_($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule120() {
         if (is_array($this->semStack[$this->stackPos-(2-2)])) { $this->semValue = array_merge($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)]); } else { $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)]; };
    }

    protected function reduceRule121() {
         $this->semValue = array();
    }

    protected function reduceRule122() {
         $startAttributes = $this->lookaheadStartAttributes; if (isset($startAttributes['comments'])) { $nop = new Stmt\Nop(['comments' => $startAttributes['comments']]); } else { $nop = null; };
            if ($nop !== null) { $this->semStack[$this->stackPos-(1-1)][] = $nop; } $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule123() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule124() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule125() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule126() {
         throw new Error('__HALT_COMPILER() can only be used from the outermost scope', $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule127() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)]; $attrs = $this->startAttributeStack[$this->stackPos-(3-1)]; $stmts = $this->semValue; if (!empty($attrs['comments']) && isset($stmts[0])) {$stmts[0]->setAttribute('comments', array_merge($attrs['comments'], $stmts[0]->getAttribute('comments', []))); };
    }

    protected function reduceRule128() {
         $this->semValue = new Stmt\If_($this->semStack[$this->stackPos-(5-2)], ['stmts' => is_array($this->semStack[$this->stackPos-(5-3)]) ? $this->semStack[$this->stackPos-(5-3)] : array($this->semStack[$this->stackPos-(5-3)]), 'elseifs' => $this->semStack[$this->stackPos-(5-4)], 'else' => $this->semStack[$this->stackPos-(5-5)]], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule129() {
         $this->semValue = new Stmt\If_($this->semStack[$this->stackPos-(8-2)], ['stmts' => $this->semStack[$this->stackPos-(8-4)], 'elseifs' => $this->semStack[$this->stackPos-(8-5)], 'else' => $this->semStack[$this->stackPos-(8-6)]], $this->startAttributeStack[$this->stackPos-(8-1)] + $this->endAttributes);
    }

    protected function reduceRule130() {
         $this->semValue = new Stmt\While_($this->semStack[$this->stackPos-(3-2)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule131() {
         $this->semValue = new Stmt\Do_($this->semStack[$this->stackPos-(5-4)], is_array($this->semStack[$this->stackPos-(5-2)]) ? $this->semStack[$this->stackPos-(5-2)] : array($this->semStack[$this->stackPos-(5-2)]), $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule132() {
         $this->semValue = new Stmt\For_(['init' => $this->semStack[$this->stackPos-(9-3)], 'cond' => $this->semStack[$this->stackPos-(9-5)], 'loop' => $this->semStack[$this->stackPos-(9-7)], 'stmts' => $this->semStack[$this->stackPos-(9-9)]], $this->startAttributeStack[$this->stackPos-(9-1)] + $this->endAttributes);
    }

    protected function reduceRule133() {
         $this->semValue = new Stmt\Switch_($this->semStack[$this->stackPos-(3-2)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule134() {
         $this->semValue = new Stmt\Break_(null, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule135() {
         $this->semValue = new Stmt\Break_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule136() {
         $this->semValue = new Stmt\Continue_(null, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule137() {
         $this->semValue = new Stmt\Continue_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule138() {
         $this->semValue = new Stmt\Return_(null, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule139() {
         $this->semValue = new Stmt\Return_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule140() {
         $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule141() {
         $this->semValue = new Stmt\Global_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule142() {
         $this->semValue = new Stmt\Static_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule143() {
         $this->semValue = new Stmt\Echo_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule144() {
         $this->semValue = new Stmt\InlineHTML($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule145() {
         $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule146() {
         $this->semValue = new Stmt\Unset_($this->semStack[$this->stackPos-(5-3)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule147() {
         $this->semValue = new Stmt\Foreach_($this->semStack[$this->stackPos-(7-3)], $this->semStack[$this->stackPos-(7-5)][0], ['keyVar' => null, 'byRef' => $this->semStack[$this->stackPos-(7-5)][1], 'stmts' => $this->semStack[$this->stackPos-(7-7)]], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes);
    }

    protected function reduceRule148() {
         $this->semValue = new Stmt\Foreach_($this->semStack[$this->stackPos-(9-3)], $this->semStack[$this->stackPos-(9-7)][0], ['keyVar' => $this->semStack[$this->stackPos-(9-5)], 'byRef' => $this->semStack[$this->stackPos-(9-7)][1], 'stmts' => $this->semStack[$this->stackPos-(9-9)]], $this->startAttributeStack[$this->stackPos-(9-1)] + $this->endAttributes);
    }

    protected function reduceRule149() {
         $this->semValue = new Stmt\Declare_($this->semStack[$this->stackPos-(5-3)], $this->semStack[$this->stackPos-(5-5)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule150() {
         $this->semValue = new Stmt\TryCatch($this->semStack[$this->stackPos-(6-3)], $this->semStack[$this->stackPos-(6-5)], $this->semStack[$this->stackPos-(6-6)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule151() {
         $this->semValue = new Stmt\Throw_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule152() {
         $this->semValue = new Stmt\Goto_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule153() {
         $this->semValue = new Stmt\Label($this->semStack[$this->stackPos-(2-1)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule154() {
         $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule155() {
         $this->semValue = array(); /* means: no statement */
    }

    protected function reduceRule156() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule157() {
         $startAttributes = $this->startAttributeStack[$this->stackPos-(1-1)]; if (isset($startAttributes['comments'])) { $this->semValue = new Stmt\Nop(['comments' => $startAttributes['comments']]); } else { $this->semValue = null; };
            if ($this->semValue === null) $this->semValue = array(); /* means: no statement */
    }

    protected function reduceRule158() {
         $this->semValue = array();
    }

    protected function reduceRule159() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule160() {
         $this->semValue = new Stmt\Catch_($this->semStack[$this->stackPos-(8-3)], substr($this->semStack[$this->stackPos-(8-4)], 1), $this->semStack[$this->stackPos-(8-7)], $this->startAttributeStack[$this->stackPos-(8-1)] + $this->endAttributes);
    }

    protected function reduceRule161() {
         $this->semValue = null;
    }

    protected function reduceRule162() {
         $this->semValue = $this->semStack[$this->stackPos-(4-3)];
    }

    protected function reduceRule163() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule164() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule165() {
         $this->semValue = false;
    }

    protected function reduceRule166() {
         $this->semValue = true;
    }

    protected function reduceRule167() {
         $this->semValue = false;
    }

    protected function reduceRule168() {
         $this->semValue = true;
    }

    protected function reduceRule169() {
         $this->semValue = new Stmt\Function_($this->semStack[$this->stackPos-(10-3)], ['byRef' => $this->semStack[$this->stackPos-(10-2)], 'params' => $this->semStack[$this->stackPos-(10-5)], 'returnType' => $this->semStack[$this->stackPos-(10-7)], 'stmts' => $this->semStack[$this->stackPos-(10-9)]], $this->startAttributeStack[$this->stackPos-(10-1)] + $this->endAttributes);
    }

    protected function reduceRule170() {
         $this->semValue = new Stmt\Class_($this->semStack[$this->stackPos-(7-2)], ['type' => $this->semStack[$this->stackPos-(7-1)], 'extends' => $this->semStack[$this->stackPos-(7-3)], 'implements' => $this->semStack[$this->stackPos-(7-4)], 'stmts' => $this->semStack[$this->stackPos-(7-6)]], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes);
    }

    protected function reduceRule171() {
         $this->semValue = new Stmt\Interface_($this->semStack[$this->stackPos-(6-2)], ['extends' => $this->semStack[$this->stackPos-(6-3)], 'stmts' => $this->semStack[$this->stackPos-(6-5)]], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule172() {
         $this->semValue = new Stmt\Trait_($this->semStack[$this->stackPos-(5-2)], $this->semStack[$this->stackPos-(5-4)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule173() {
         $this->semValue = 0;
    }

    protected function reduceRule174() {
         $this->semValue = Stmt\Class_::MODIFIER_ABSTRACT;
    }

    protected function reduceRule175() {
         $this->semValue = Stmt\Class_::MODIFIER_FINAL;
    }

    protected function reduceRule176() {
         $this->semValue = null;
    }

    protected function reduceRule177() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule178() {
         $this->semValue = array();
    }

    protected function reduceRule179() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule180() {
         $this->semValue = array();
    }

    protected function reduceRule181() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule182() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule183() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule184() {
         $this->semValue = is_array($this->semStack[$this->stackPos-(1-1)]) ? $this->semStack[$this->stackPos-(1-1)] : array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule185() {
         $this->semValue = $this->semStack[$this->stackPos-(4-2)];
    }

    protected function reduceRule186() {
         $this->semValue = is_array($this->semStack[$this->stackPos-(1-1)]) ? $this->semStack[$this->stackPos-(1-1)] : array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule187() {
         $this->semValue = $this->semStack[$this->stackPos-(4-2)];
    }

    protected function reduceRule188() {
         $this->semValue = is_array($this->semStack[$this->stackPos-(1-1)]) ? $this->semStack[$this->stackPos-(1-1)] : array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule189() {
         $this->semValue = null;
    }

    protected function reduceRule190() {
         $this->semValue = $this->semStack[$this->stackPos-(4-2)];
    }

    protected function reduceRule191() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule192() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule193() {
         $this->semValue = new Stmt\DeclareDeclare($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule194() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule195() {
         $this->semValue = $this->semStack[$this->stackPos-(4-3)];
    }

    protected function reduceRule196() {
         $this->semValue = $this->semStack[$this->stackPos-(4-2)];
    }

    protected function reduceRule197() {
         $this->semValue = $this->semStack[$this->stackPos-(5-3)];
    }

    protected function reduceRule198() {
         $this->semValue = array();
    }

    protected function reduceRule199() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule200() {
         $this->semValue = new Stmt\Case_($this->semStack[$this->stackPos-(4-2)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule201() {
         $this->semValue = new Stmt\Case_(null, $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule202() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule203() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule204() {
         $this->semValue = is_array($this->semStack[$this->stackPos-(1-1)]) ? $this->semStack[$this->stackPos-(1-1)] : array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule205() {
         $this->semValue = $this->semStack[$this->stackPos-(4-2)];
    }

    protected function reduceRule206() {
         $this->semValue = array();
    }

    protected function reduceRule207() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule208() {
         $this->semValue = new Stmt\ElseIf_($this->semStack[$this->stackPos-(3-2)], is_array($this->semStack[$this->stackPos-(3-3)]) ? $this->semStack[$this->stackPos-(3-3)] : array($this->semStack[$this->stackPos-(3-3)]), $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule209() {
         $this->semValue = array();
    }

    protected function reduceRule210() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule211() {
         $this->semValue = new Stmt\ElseIf_($this->semStack[$this->stackPos-(4-2)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule212() {
         $this->semValue = null;
    }

    protected function reduceRule213() {
         $this->semValue = new Stmt\Else_(is_array($this->semStack[$this->stackPos-(2-2)]) ? $this->semStack[$this->stackPos-(2-2)] : array($this->semStack[$this->stackPos-(2-2)]), $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule214() {
         $this->semValue = null;
    }

    protected function reduceRule215() {
         $this->semValue = new Stmt\Else_($this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule216() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)], false);
    }

    protected function reduceRule217() {
         $this->semValue = array($this->semStack[$this->stackPos-(2-2)], true);
    }

    protected function reduceRule218() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)], false);
    }

    protected function reduceRule219() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule220() {
         $this->semValue = array();
    }

    protected function reduceRule221() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule222() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule223() {
         $this->semValue = new Node\Param(substr($this->semStack[$this->stackPos-(4-4)], 1), null, $this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-2)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule224() {
         $this->semValue = new Node\Param(substr($this->semStack[$this->stackPos-(6-4)], 1), $this->semStack[$this->stackPos-(6-6)], $this->semStack[$this->stackPos-(6-1)], $this->semStack[$this->stackPos-(6-2)], $this->semStack[$this->stackPos-(6-3)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule225() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule226() {
         $this->semValue = 'array';
    }

    protected function reduceRule227() {
         $this->semValue = 'callable';
    }

    protected function reduceRule228() {
         $this->semValue = null;
    }

    protected function reduceRule229() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule230() {
         $this->semValue = null;
    }

    protected function reduceRule231() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule232() {
         $this->semValue = array();
    }

    protected function reduceRule233() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule234() {
         $this->semValue = array(new Node\Arg($this->semStack[$this->stackPos-(3-2)], false, false, $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes));
    }

    protected function reduceRule235() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule236() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule237() {
         $this->semValue = new Node\Arg($this->semStack[$this->stackPos-(1-1)], false, false, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule238() {
         $this->semValue = new Node\Arg($this->semStack[$this->stackPos-(2-2)], true, false, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule239() {
         $this->semValue = new Node\Arg($this->semStack[$this->stackPos-(2-2)], false, true, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule240() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule241() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule242() {
         $this->semValue = new Expr\Variable(substr($this->semStack[$this->stackPos-(1-1)], 1), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule243() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule244() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule245() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule246() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule247() {
         $this->semValue = new Stmt\StaticVar(substr($this->semStack[$this->stackPos-(1-1)], 1), null, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule248() {
         $this->semValue = new Stmt\StaticVar(substr($this->semStack[$this->stackPos-(3-1)], 1), $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule249() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule250() {
         $this->semValue = array();
    }

    protected function reduceRule251() {
         $this->semValue = new Stmt\Property($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule252() {
         $this->semValue = new Stmt\ClassConst($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule253() {
         $this->semValue = new Stmt\ClassMethod($this->semStack[$this->stackPos-(9-4)], ['type' => $this->semStack[$this->stackPos-(9-1)], 'byRef' => $this->semStack[$this->stackPos-(9-3)], 'params' => $this->semStack[$this->stackPos-(9-6)], 'returnType' => $this->semStack[$this->stackPos-(9-8)], 'stmts' => $this->semStack[$this->stackPos-(9-9)]], $this->startAttributeStack[$this->stackPos-(9-1)] + $this->endAttributes);
    }

    protected function reduceRule254() {
         $this->semValue = new Stmt\TraitUse($this->semStack[$this->stackPos-(3-2)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule255() {
         $this->semValue = array();
    }

    protected function reduceRule256() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule257() {
         $this->semValue = array();
    }

    protected function reduceRule258() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule259() {
         $this->semValue = new Stmt\TraitUseAdaptation\Precedence($this->semStack[$this->stackPos-(4-1)][0], $this->semStack[$this->stackPos-(4-1)][1], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule260() {
         $this->semValue = new Stmt\TraitUseAdaptation\Alias($this->semStack[$this->stackPos-(5-1)][0], $this->semStack[$this->stackPos-(5-1)][1], $this->semStack[$this->stackPos-(5-3)], $this->semStack[$this->stackPos-(5-4)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule261() {
         $this->semValue = new Stmt\TraitUseAdaptation\Alias($this->semStack[$this->stackPos-(4-1)][0], $this->semStack[$this->stackPos-(4-1)][1], $this->semStack[$this->stackPos-(4-3)], null, $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule262() {
         $this->semValue = new Stmt\TraitUseAdaptation\Alias($this->semStack[$this->stackPos-(4-1)][0], $this->semStack[$this->stackPos-(4-1)][1], null, $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule263() {
         $this->semValue = new Stmt\TraitUseAdaptation\Alias($this->semStack[$this->stackPos-(4-1)][0], $this->semStack[$this->stackPos-(4-1)][1], null, $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule264() {
         $this->semValue = array($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)]);
    }

    protected function reduceRule265() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule266() {
         $this->semValue = array(null, $this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule267() {
         $this->semValue = null;
    }

    protected function reduceRule268() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule269() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule270() {
         $this->semValue = 0;
    }

    protected function reduceRule271() {
         $this->semValue = 0;
    }

    protected function reduceRule272() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule273() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule274() {
         Stmt\Class_::verifyModifier($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)]); $this->semValue = $this->semStack[$this->stackPos-(2-1)] | $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule275() {
         $this->semValue = Stmt\Class_::MODIFIER_PUBLIC;
    }

    protected function reduceRule276() {
         $this->semValue = Stmt\Class_::MODIFIER_PROTECTED;
    }

    protected function reduceRule277() {
         $this->semValue = Stmt\Class_::MODIFIER_PRIVATE;
    }

    protected function reduceRule278() {
         $this->semValue = Stmt\Class_::MODIFIER_STATIC;
    }

    protected function reduceRule279() {
         $this->semValue = Stmt\Class_::MODIFIER_ABSTRACT;
    }

    protected function reduceRule280() {
         $this->semValue = Stmt\Class_::MODIFIER_FINAL;
    }

    protected function reduceRule281() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule282() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule283() {
         $this->semValue = new Stmt\PropertyProperty(substr($this->semStack[$this->stackPos-(1-1)], 1), null, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule284() {
         $this->semValue = new Stmt\PropertyProperty(substr($this->semStack[$this->stackPos-(3-1)], 1), $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule285() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule286() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule287() {
         $this->semValue = array();
    }

    protected function reduceRule288() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule289() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule290() {
         $this->semValue = new Expr\Assign($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule291() {
         $this->semValue = new Expr\Assign($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule292() {
         $this->semValue = new Expr\AssignRef($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule293() {
         $this->semValue = new Expr\AssignRef($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule294() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule295() {
         $this->semValue = new Expr\Clone_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule296() {
         $this->semValue = new Expr\AssignOp\Plus($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule297() {
         $this->semValue = new Expr\AssignOp\Minus($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule298() {
         $this->semValue = new Expr\AssignOp\Mul($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule299() {
         $this->semValue = new Expr\AssignOp\Div($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule300() {
         $this->semValue = new Expr\AssignOp\Concat($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule301() {
         $this->semValue = new Expr\AssignOp\Mod($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule302() {
         $this->semValue = new Expr\AssignOp\BitwiseAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule303() {
         $this->semValue = new Expr\AssignOp\BitwiseOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule304() {
         $this->semValue = new Expr\AssignOp\BitwiseXor($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule305() {
         $this->semValue = new Expr\AssignOp\ShiftLeft($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule306() {
         $this->semValue = new Expr\AssignOp\ShiftRight($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule307() {
         $this->semValue = new Expr\AssignOp\Pow($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule308() {
         $this->semValue = new Expr\PostInc($this->semStack[$this->stackPos-(2-1)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule309() {
         $this->semValue = new Expr\PreInc($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule310() {
         $this->semValue = new Expr\PostDec($this->semStack[$this->stackPos-(2-1)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule311() {
         $this->semValue = new Expr\PreDec($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule312() {
         $this->semValue = new Expr\BinaryOp\BooleanOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule313() {
         $this->semValue = new Expr\BinaryOp\BooleanAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule314() {
         $this->semValue = new Expr\BinaryOp\LogicalOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule315() {
         $this->semValue = new Expr\BinaryOp\LogicalAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule316() {
         $this->semValue = new Expr\BinaryOp\LogicalXor($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule317() {
         $this->semValue = new Expr\BinaryOp\BitwiseOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule318() {
         $this->semValue = new Expr\BinaryOp\BitwiseAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule319() {
         $this->semValue = new Expr\BinaryOp\BitwiseXor($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule320() {
         $this->semValue = new Expr\BinaryOp\Concat($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule321() {
         $this->semValue = new Expr\BinaryOp\Plus($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule322() {
         $this->semValue = new Expr\BinaryOp\Minus($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule323() {
         $this->semValue = new Expr\BinaryOp\Mul($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule324() {
         $this->semValue = new Expr\BinaryOp\Div($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule325() {
         $this->semValue = new Expr\BinaryOp\Mod($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule326() {
         $this->semValue = new Expr\BinaryOp\ShiftLeft($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule327() {
         $this->semValue = new Expr\BinaryOp\ShiftRight($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule328() {
         $this->semValue = new Expr\BinaryOp\Pow($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule329() {
         $this->semValue = new Expr\UnaryPlus($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule330() {
         $this->semValue = new Expr\UnaryMinus($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule331() {
         $this->semValue = new Expr\BooleanNot($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule332() {
         $this->semValue = new Expr\BitwiseNot($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule333() {
         $this->semValue = new Expr\BinaryOp\Identical($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule334() {
         $this->semValue = new Expr\BinaryOp\NotIdentical($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule335() {
         $this->semValue = new Expr\BinaryOp\Equal($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule336() {
         $this->semValue = new Expr\BinaryOp\NotEqual($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule337() {
         $this->semValue = new Expr\BinaryOp\Spaceship($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule338() {
         $this->semValue = new Expr\BinaryOp\Smaller($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule339() {
         $this->semValue = new Expr\BinaryOp\SmallerOrEqual($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule340() {
         $this->semValue = new Expr\BinaryOp\Greater($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule341() {
         $this->semValue = new Expr\BinaryOp\GreaterOrEqual($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule342() {
         $this->semValue = new Expr\Instanceof_($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule343() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule344() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule345() {
         $this->semValue = new Expr\Ternary($this->semStack[$this->stackPos-(5-1)], $this->semStack[$this->stackPos-(5-3)], $this->semStack[$this->stackPos-(5-5)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule346() {
         $this->semValue = new Expr\Ternary($this->semStack[$this->stackPos-(4-1)], null, $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule347() {
         $this->semValue = new Expr\BinaryOp\Coalesce($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule348() {
         $this->semValue = new Expr\Isset_($this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule349() {
         $this->semValue = new Expr\Empty_($this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule350() {
         $this->semValue = new Expr\Include_($this->semStack[$this->stackPos-(2-2)], Expr\Include_::TYPE_INCLUDE, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule351() {
         $this->semValue = new Expr\Include_($this->semStack[$this->stackPos-(2-2)], Expr\Include_::TYPE_INCLUDE_ONCE, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule352() {
         $this->semValue = new Expr\Eval_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule353() {
         $this->semValue = new Expr\Include_($this->semStack[$this->stackPos-(2-2)], Expr\Include_::TYPE_REQUIRE, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule354() {
         $this->semValue = new Expr\Include_($this->semStack[$this->stackPos-(2-2)], Expr\Include_::TYPE_REQUIRE_ONCE, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule355() {
         $this->semValue = new Expr\Cast\Int_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule356() {
         $this->semValue = new Expr\Cast\Double($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule357() {
         $this->semValue = new Expr\Cast\String_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule358() {
         $this->semValue = new Expr\Cast\Array_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule359() {
         $this->semValue = new Expr\Cast\Object_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule360() {
         $this->semValue = new Expr\Cast\Bool_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule361() {
         $this->semValue = new Expr\Cast\Unset_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule362() {
         $attrs = $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes;
            $attrs['kind'] = strtolower($this->semStack[$this->stackPos-(2-1)]) === 'exit' ? Expr\Exit_::KIND_EXIT : Expr\Exit_::KIND_DIE;
            $this->semValue = new Expr\Exit_($this->semStack[$this->stackPos-(2-2)], $attrs);
    }

    protected function reduceRule363() {
         $this->semValue = new Expr\ErrorSuppress($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule364() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule365() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule366() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule367() {
         $this->semValue = new Expr\ShellExec($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule368() {
         $this->semValue = new Expr\Print_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule369() {
         $this->semValue = new Expr\Yield_(null, null, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule370() {
         $this->semValue = new Expr\YieldFrom($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule371() {
         $this->semValue = new Expr\Closure(['static' => false, 'byRef' => $this->semStack[$this->stackPos-(10-2)], 'params' => $this->semStack[$this->stackPos-(10-4)], 'uses' => $this->semStack[$this->stackPos-(10-6)], 'returnType' => $this->semStack[$this->stackPos-(10-7)], 'stmts' => $this->semStack[$this->stackPos-(10-9)]], $this->startAttributeStack[$this->stackPos-(10-1)] + $this->endAttributes);
    }

    protected function reduceRule372() {
         $this->semValue = new Expr\Closure(['static' => true, 'byRef' => $this->semStack[$this->stackPos-(11-3)], 'params' => $this->semStack[$this->stackPos-(11-5)], 'uses' => $this->semStack[$this->stackPos-(11-7)], 'returnType' => $this->semStack[$this->stackPos-(11-8)], 'stmts' => $this->semStack[$this->stackPos-(11-10)]], $this->startAttributeStack[$this->stackPos-(11-1)] + $this->endAttributes);
    }

    protected function reduceRule373() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule374() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule375() {
         $this->semValue = new Expr\Yield_($this->semStack[$this->stackPos-(2-2)], null, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule376() {
         $this->semValue = new Expr\Yield_($this->semStack[$this->stackPos-(4-4)], $this->semStack[$this->stackPos-(4-2)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule377() {
         $attrs = $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes; $attrs['kind'] = Expr\Array_::KIND_LONG;
            $this->semValue = new Expr\Array_($this->semStack[$this->stackPos-(4-3)], $attrs);
    }

    protected function reduceRule378() {
         $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = Expr\Array_::KIND_SHORT;
            $this->semValue = new Expr\Array_($this->semStack[$this->stackPos-(3-2)], $attrs);
    }

    protected function reduceRule379() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule380() {
         $attrs = $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes; $attrs['kind'] = ($this->semStack[$this->stackPos-(4-1)][0] === "'" || ($this->semStack[$this->stackPos-(4-1)][1] === "'" && ($this->semStack[$this->stackPos-(4-1)][0] === 'b' || $this->semStack[$this->stackPos-(4-1)][0] === 'B')) ? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED);
            $this->semValue = new Expr\ArrayDimFetch(new Scalar\String_(Scalar\String_::parse($this->semStack[$this->stackPos-(4-1)]), $attrs), $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule381() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule382() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule383() {
         $this->semValue = array(new Stmt\Class_(null, ['type' => 0, 'extends' => $this->semStack[$this->stackPos-(7-3)], 'implements' => $this->semStack[$this->stackPos-(7-4)], 'stmts' => $this->semStack[$this->stackPos-(7-6)]], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(7-2)]);
    }

    protected function reduceRule384() {
         $this->semValue = new Expr\New_($this->semStack[$this->stackPos-(3-2)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule385() {
         list($class, $ctorArgs) = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = new Expr\New_($class, $ctorArgs, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule386() {
         $this->semValue = array();
    }

    protected function reduceRule387() {
         $this->semValue = $this->semStack[$this->stackPos-(4-3)];
    }

    protected function reduceRule388() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule389() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule390() {
         $this->semValue = new Expr\ClosureUse(substr($this->semStack[$this->stackPos-(2-2)], 1), $this->semStack[$this->stackPos-(2-1)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule391() {
         $this->semValue = new Expr\FuncCall($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule392() {
         $this->semValue = new Expr\StaticCall($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule393() {
         $this->semValue = new Expr\StaticCall($this->semStack[$this->stackPos-(6-1)], $this->semStack[$this->stackPos-(6-4)], $this->semStack[$this->stackPos-(6-6)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule394() {

            if ($this->semStack[$this->stackPos-(2-1)] instanceof Node\Expr\StaticPropertyFetch) {
                $this->semValue = new Expr\StaticCall($this->semStack[$this->stackPos-(2-1)]->class, new Expr\Variable($this->semStack[$this->stackPos-(2-1)]->name, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
            } elseif ($this->semStack[$this->stackPos-(2-1)] instanceof Node\Expr\ArrayDimFetch) {
                $tmp = $this->semStack[$this->stackPos-(2-1)];
                while ($tmp->var instanceof Node\Expr\ArrayDimFetch) {
                    $tmp = $tmp->var;
                }

                $this->semValue = new Expr\StaticCall($tmp->var->class, $this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
                $tmp->var = new Expr\Variable($tmp->var->name, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
            } else {
                throw new \Exception;
            }

    }

    protected function reduceRule395() {
         $this->semValue = new Expr\FuncCall($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule396() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule397() {
         $this->semValue = new Name($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule398() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule399() {
         $this->semValue = new Name($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule400() {
         $this->semValue = new Name\FullyQualified($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule401() {
         $this->semValue = new Name\Relative($this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule402() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule403() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule404() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule405() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule406() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule407() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule408() {
         $this->semValue = new Expr\PropertyFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule409() {
         $this->semValue = new Expr\PropertyFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule410() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule411() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule412() {
         $this->semValue = null;
    }

    protected function reduceRule413() {
         $this->semValue = null;
    }

    protected function reduceRule414() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule415() {
         $this->semValue = array();
    }

    protected function reduceRule416() {
         $this->semValue = array(new Scalar\EncapsedStringPart(Scalar\String_::parseEscapeSequences($this->semStack[$this->stackPos-(1-1)], '`', false), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes));
    }

    protected function reduceRule417() {
         foreach ($this->semStack[$this->stackPos-(1-1)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, '`', false); } }; $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule418() {
         $this->semValue = array();
    }

    protected function reduceRule419() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule420() {
         $this->semValue = Scalar\LNumber::fromString($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes, true);
    }

    protected function reduceRule421() {
         $this->semValue = new Scalar\DNumber(Scalar\DNumber::parse($this->semStack[$this->stackPos-(1-1)]), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule422() {
         $attrs = $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes; $attrs['kind'] = ($this->semStack[$this->stackPos-(1-1)][0] === "'" || ($this->semStack[$this->stackPos-(1-1)][1] === "'" && ($this->semStack[$this->stackPos-(1-1)][0] === 'b' || $this->semStack[$this->stackPos-(1-1)][0] === 'B')) ? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED);
            $this->semValue = new Scalar\String_(Scalar\String_::parse($this->semStack[$this->stackPos-(1-1)], false), $attrs);
    }

    protected function reduceRule423() {
         $this->semValue = new Scalar\MagicConst\Line($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule424() {
         $this->semValue = new Scalar\MagicConst\File($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule425() {
         $this->semValue = new Scalar\MagicConst\Dir($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule426() {
         $this->semValue = new Scalar\MagicConst\Class_($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule427() {
         $this->semValue = new Scalar\MagicConst\Trait_($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule428() {
         $this->semValue = new Scalar\MagicConst\Method($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule429() {
         $this->semValue = new Scalar\MagicConst\Function_($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule430() {
         $this->semValue = new Scalar\MagicConst\Namespace_($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule431() {
         $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];;
            $this->semValue = new Scalar\String_(Scalar\String_::parseDocString($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-2)], false), $attrs);
    }

    protected function reduceRule432() {
         $attrs = $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(2-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(2-1)], $matches); $attrs['docLabel'] = $matches[1];;
            $this->semValue = new Scalar\String_('', $attrs);
    }

    protected function reduceRule433() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule434() {
         $this->semValue = new Expr\ClassConstFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule435() {
         $this->semValue = new Expr\ConstFetch($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule436() {
         $this->semValue = new Expr\Array_($this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule437() {
         $this->semValue = new Expr\Array_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule438() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule439() {
         $this->semValue = new Expr\BinaryOp\BooleanOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule440() {
         $this->semValue = new Expr\BinaryOp\BooleanAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule441() {
         $this->semValue = new Expr\BinaryOp\LogicalOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule442() {
         $this->semValue = new Expr\BinaryOp\LogicalAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule443() {
         $this->semValue = new Expr\BinaryOp\LogicalXor($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule444() {
         $this->semValue = new Expr\BinaryOp\BitwiseOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule445() {
         $this->semValue = new Expr\BinaryOp\BitwiseAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule446() {
         $this->semValue = new Expr\BinaryOp\BitwiseXor($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule447() {
         $this->semValue = new Expr\BinaryOp\Concat($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule448() {
         $this->semValue = new Expr\BinaryOp\Plus($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule449() {
         $this->semValue = new Expr\BinaryOp\Minus($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule450() {
         $this->semValue = new Expr\BinaryOp\Mul($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule451() {
         $this->semValue = new Expr\BinaryOp\Div($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule452() {
         $this->semValue = new Expr\BinaryOp\Mod($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule453() {
         $this->semValue = new Expr\BinaryOp\ShiftLeft($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule454() {
         $this->semValue = new Expr\BinaryOp\ShiftRight($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule455() {
         $this->semValue = new Expr\BinaryOp\Pow($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule456() {
         $this->semValue = new Expr\UnaryPlus($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule457() {
         $this->semValue = new Expr\UnaryMinus($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule458() {
         $this->semValue = new Expr\BooleanNot($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule459() {
         $this->semValue = new Expr\BitwiseNot($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule460() {
         $this->semValue = new Expr\BinaryOp\Identical($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule461() {
         $this->semValue = new Expr\BinaryOp\NotIdentical($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule462() {
         $this->semValue = new Expr\BinaryOp\Equal($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule463() {
         $this->semValue = new Expr\BinaryOp\NotEqual($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule464() {
         $this->semValue = new Expr\BinaryOp\Smaller($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule465() {
         $this->semValue = new Expr\BinaryOp\SmallerOrEqual($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule466() {
         $this->semValue = new Expr\BinaryOp\Greater($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule467() {
         $this->semValue = new Expr\BinaryOp\GreaterOrEqual($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule468() {
         $this->semValue = new Expr\Ternary($this->semStack[$this->stackPos-(5-1)], $this->semStack[$this->stackPos-(5-3)], $this->semStack[$this->stackPos-(5-5)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule469() {
         $this->semValue = new Expr\Ternary($this->semStack[$this->stackPos-(4-1)], null, $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule470() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule471() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule472() {
         $this->semValue = new Expr\ConstFetch($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule473() {
         $this->semValue = new Expr\ClassConstFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule474() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule475() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule476() {
         $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = Scalar\String_::KIND_DOUBLE_QUOTED;
            foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, '"', true); } }; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $attrs);
    }

    protected function reduceRule477() {
         $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];;
            foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, null, true); } } $s->value = preg_replace('~(\r\n|\n|\r)\z~', '', $s->value); if ('' === $s->value) array_pop($this->semStack[$this->stackPos-(3-2)]);; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $attrs);
    }

    protected function reduceRule478() {
         $this->semValue = array();
    }

    protected function reduceRule479() {
         $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule480() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule481() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule482() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule483() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule484() {
         $this->semValue = new Expr\ArrayItem($this->semStack[$this->stackPos-(3-3)], $this->semStack[$this->stackPos-(3-1)], false, $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule485() {
         $this->semValue = new Expr\ArrayItem($this->semStack[$this->stackPos-(1-1)], null, false, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule486() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule487() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule488() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule489() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule490() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(6-2)], $this->semStack[$this->stackPos-(6-5)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule491() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule492() {
         $this->semValue = new Expr\PropertyFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule493() {
         $this->semValue = new Expr\MethodCall($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule494() {
         $this->semValue = new Expr\FuncCall($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule495() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule496() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule497() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule498() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule499() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule500() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule501() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule502() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule503() {
         $this->semValue = new Expr\StaticPropertyFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule504() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule505() {
         $this->semValue = new Expr\StaticPropertyFetch($this->semStack[$this->stackPos-(3-1)], substr($this->semStack[$this->stackPos-(3-3)], 1), $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule506() {
         $this->semValue = new Expr\StaticPropertyFetch($this->semStack[$this->stackPos-(6-1)], $this->semStack[$this->stackPos-(6-5)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule507() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule508() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule509() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule510() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule511() {
         $this->semValue = new Expr\Variable(substr($this->semStack[$this->stackPos-(1-1)], 1), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule512() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule513() {
         $this->semValue = null;
    }

    protected function reduceRule514() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule515() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule516() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule517() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule518() {
         $this->semValue = new Expr\List_($this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule519() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule520() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule521() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule522() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule523() {
         $this->semValue = null;
    }

    protected function reduceRule524() {
         $this->semValue = array();
    }

    protected function reduceRule525() {
         $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule526() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule527() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule528() {
         $this->semValue = new Expr\ArrayItem($this->semStack[$this->stackPos-(3-3)], $this->semStack[$this->stackPos-(3-1)], false, $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule529() {
         $this->semValue = new Expr\ArrayItem($this->semStack[$this->stackPos-(1-1)], null, false, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule530() {
         $this->semValue = new Expr\ArrayItem($this->semStack[$this->stackPos-(4-4)], $this->semStack[$this->stackPos-(4-1)], true, $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule531() {
         $this->semValue = new Expr\ArrayItem($this->semStack[$this->stackPos-(2-2)], null, true, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule532() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule533() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule534() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule535() {
         $this->semValue = array($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)]);
    }

    protected function reduceRule536() {
         $this->semValue = new Scalar\EncapsedStringPart($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule537() {
         $this->semValue = new Expr\Variable(substr($this->semStack[$this->stackPos-(1-1)], 1), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule538() {
         $this->semValue = new Expr\ArrayDimFetch(new Expr\Variable(substr($this->semStack[$this->stackPos-(4-1)], 1), $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule539() {
         $this->semValue = new Expr\PropertyFetch(new Expr\Variable(substr($this->semStack[$this->stackPos-(3-1)], 1), $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule540() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule541() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule542() {
         $this->semValue = new Expr\ArrayDimFetch(new Expr\Variable($this->semStack[$this->stackPos-(6-2)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(6-4)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule543() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule544() {
         $this->semValue = new Scalar\String_($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule545() {
         $this->semValue = new Scalar\String_($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule546() {
         $this->semValue = new Expr\Variable(substr($this->semStack[$this->stackPos-(1-1)], 1), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }
}
'",
        "T_IS_GREATER_OR_EQUAL",
        "T_SL",
        "T_SR",
        "'+'",
        "'-'",
        "'.'",
        "'*'",
        "'/'",
        "'%'",
        "'!'",
        "T_INSTANCEOF",
        "'~'",
        "T_INC",
        "T_DEC",
        "T_INT_CAST",
        "T_DOUBLE_CAST",
        "T_STRING_CAST",
        "T_ARRAY_CAST",
        "T_OBJECT_CAST",
        "T_BOOL_CAST",
        "T_UNSET_CAST",
        "'@'",
        "T_POW",
        "'['",
        "T_NEW",
        "T_CLONE",
        "T_EXIT",
        "T_IF",
        "T_ELSEIF",
        "T_ELSE",
        "T_ENDIF",
        "T_LNUMBER",
        "T_DNUMBER",
        "T_STRING",
        "T_STRING_VARNAME",
        "T_VARIABLE",
        "T_NUM_STRING",
        "T_INLINE_HTML",
        "T_ENCAPSED_AND_WHITESPACE",
        "T_CONSTANT_ENCAPSED_STRING",
        "T_ECHO",
        "T_DO",
        "T_WHILE",
        "T_ENDWHILE",
        "T_FOR",
        "T_ENDFOR",
        "T_FOREACH",
        "T_ENDFOREACH",
        "T_DECLARE",
        "T_ENDDECLARE",
        "T_AS",
        "T_SWITCH",
        "T_ENDSWITCH",
        "T_CASE",
        "T_DEFAULT",
        "T_BREAK",
        "T_CONTINUE",
        "T_GOTO",
        "T_FUNCTION",
        "T_CONST",
        "T_RETURN",
        "T_TRY",
        "T_CATCH",
        "T_FINALLY",
        "T_THROW",
        "T_USE",
        "T_INSTEADOF",
        "T_GLOBAL",
        "T_STATIC",
        "T_ABSTRACT",
        "T_FINAL",
        "T_PRIVATE",
        "T_PROTECTED",
        "T_PUBLIC",
        "T_VAR",
        "T_UNSET",
        "T_ISSET",
        "T_EMPTY",
        "T_HALT_COMPILER",
        "T_CLASS",
        "T_TRAIT",
        "T_INTERFACE",
        "T_EXTENDS",
        "T_IMPLEMENTS",
        "T_OBJECT_OPERATOR",
        "T_LIST",
        "T_ARRAY",
        "T_CALLABLE",
        "T_CLASS_C",
        "T_TRAIT_C",
        "T_METHOD_C",
        "T_FUNC_C",
        "T_LINE",
        "T_FILE",
        "T_START_HEREDOC",
        "T_END_HEREDOC",
        "T_DOLLAR_OPEN_CURLY_BRACES",
        "T_CURLY_OPEN",
        "T_PAAMAYIM_NEKUDOTAYIM",
        "T_NAMESPACE",
        "T_NS_C",
        "T_DIR",
        "T_NS_SEPARATOR",
        "T_ELLIPSIS",
        "';'",
        "'{'",
        "'}'",
        "'('",
        "')'",
        "'`'",
        "']'",
        "'\"'",
        "'$'"
    );

    protected $tokenToSymbol = array(
            0,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,   53,  155,  157,  156,   52,   35,  157,
          151,  152,   50,   47,    7,   48,   49,   51,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,   29,  148,
           41,   15,   43,   28,   65,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,   67,  157,  154,   34,  157,  153,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  149,   33,  150,   55,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,  157,  157,  157,  157,
          157,  157,  157,  157,  157,  157,    1,    2,    3,    4,
            5,    6,    8,    9,   10,   11,   12,   13,   14,   16,
           17,   18,   19,   20,   21,   22,   23,   24,   25,   26,
           27,   30,   31,   32,   36,   37,   38,   39,   40,   42,
           44,   45,   46,   54,   56,   57,   58,   59,   60,   61,
           62,   63,   64,   66,   68,   69,   70,   71,   72,   73,
           74,   75,   76,   77,   78,   79,   80,   81,  157,  157,
           82,   83,   84,   85,   86,   87,   88,   89,   90,   91,
           92,   93,   94,   95,   96,   97,   98,   99,  100,  101,
          102,  103,  104,  105,  106,  107,  108,  109,  110,  111,
          112,  113,  114,  115,  116,  117,  118,  119,  120,  121,
          122,  123,  124,  125,  126,  127,  128,  129,  130,  131,
          132,  133,  134,  135,  136,  137,  157,  157,  157,  157,
          157,  157,  138,  139,  140,  141,  142,  143,  144,  145,
          146,  147
    );

    protected $action = array(
          559,  560,  561,  562,  563,  704,  564,  565,  566,  602,
          603,-32766,-32766,-32766,-32767,-32767,-32767,-32767,   88,   89,
           90,   91,   92,  520,-32766,-32766,-32766,-32766,-32766,-32766,
            0,-32766,  111,-32766,-32766,-32766,-32766,-32766,-32766,-32767,
        -32767,-32767,-32767,-32767,-32766,  335,-32766,-32766,-32766,-32766,
        -32766,-32766,  567,-32766,-32766,-32766,-32766,  279,  825,  826,
          827,  824,  823,  822,-32766,-32766,  568,  569,  570,  571,
          572,  573,  574,  265,-32766,  634,-32766,-32766,-32766,-32766,
        -32766,  258,  575,  576,  577,  578,  579,  580,  581,  582,
          583,  584,  585,  605,  606,  607,  608,  609,  597,  598,
          599,  600,  601,  586,  587,  588,  589,  590,  591,  592,
          628,  629,  630,  631,  632,  633,  593,  594,  595,  596,
          626,  617,  615,  616,  612,  613,   24,  604,  610,  611,
          618,  619,  621,  620,  622,  623,   40,   41,  373,   42,
           43,  614,  625,  624,    6,   44,   45,    7,   46, -262,
          261, -418,  695,  825,  826,  827,  824,  823,  822,  817,
          113,   22,   93,   94,   95,  636,  235,  985,  236,   22,
          650,  651, 1027,-32766, 1029, 1028, -417,  949,   96,-32766,
          985,   47,   48,  509, -233,  949,  219,   49,-32766,   50,
          214,  215,   51,   52,   53,   54,   55,   56,   57,   58,
          929,   22,  229,   59,  342,-32766,-32766,-32766, -450,  950,
          951,  636, -418,  985,  330, -459,  221,  949,-32766,-32766,
        -32766,  705, -160,  390,  391,-32766, -418,-32766,-32766,-32766,
        -32766,  400,  391, -418,  344, -421,  346, -417,-32766,  209,
        -32766,-32766,-32766,  361,  267,   63,  399,   28,  359,  510,
          118, -417,  344,   63,  386,  387,  803,  267, -417,  128,
         -420,  370,  219,  390,  391,   39,  955,  956,  957,  958,
          952,  953,  237,-32766,-32766,-32766, -460,  400,  959,  954,
          344,  126,-32766,-32766,-32766,   61,  211,  247,  799,  248,
          267,  374, -122, -122, -122,   -4,  705,  375,  117,  282,
         -416,  694,-32766,  782,   32,   17,  376, -122,  377, -122,
          378, -122,  379, -122,  985,  380, -122, -122, -122,   33,
           34,  381,  343,  752,   35,  382,  251,  299,   60, -233,
         1019,  280,  281,  383,  384,-32766,-32766,-32766,-32766,  385,
          288,   21,  680,  723,  388,  389,  341,  112,   90,   91,
           92,  269,   37, -450,  354,-32766,  122,-32766,-32766,-32766,
         -459, -416, -459,  362,  537, -159,  374, -160,  707,  525,
         -122,-32766,  375,  353,  117, -416,  694,  125, -212,   32,
           17,  376, -416,  377,  222,  378,  121,  379,   25,  217,
          380,  267,-32766,   16,   33,   34,  381,  343,  336,   35,
          382,  998,  798,   60,  246,  705,  280,  281,  383,  384,
          776,  777,  447,  250,  385,-32766,   22,  642,  723,  388,
          389, -460,  115, -460,  114,  426,   70,   71,   72,  494,
          495, 1001,  949,  529,  110,  123,-32766,  109,  263, 1024,
          691,  542,  753,  707,  525,   -4,   26,  235,   73,   74,
           75,   76,   77,   78,   79,   80,   81,   82,   83,   84,
           85,   86,   87,   88,   89,   90,   91,   92,   93,   94,
           95,  116,  235,  119,  705,  374,  238,  350,  390,  391,
          527,  375,  963,  703,   96,  694,  783,-32766,   32,   17,
          376,  922,  377,  216,  378,  692,  379,  484,   18,  380,
           63,  220,  530,   33,   34,  381,  705,  716,   35,  382,
         -159,  218,   60,   38,  649,  280,  281,  124,  290,   96,
        -32766,  650,  651,  385,  802,  553,  504,  479,  480,  814,
          543,  643,  528,  439,  531,  309,  438,  425,  776,  777,
          420,  419,  351,  430,  374,  349,  637,  663,  636,-32766,
          375, 1022,  707,  525,  694,  489,  424,   32,   17,  376,
         -216,  377,  508,  378,-32766,  379,  925,  493,  380,  482,
          435,  519,   33,   34,  381,  705,  374,   35,  382,  485,
          505,   60,  375,  348,  280,  281,  694,  -80,  208,   32,
           17,  376,  385,  377,  442,  378,   10,  379,  368,  498,
          380,  262,  490,  538,   33,   34,  381,  705,  477,   35,
          382,  259,  264,   60,  965,    0,  280,  281,  725,  962,
          337,  707,  525,    0,  385,  260,  718,  724,  710,    0,
            0,    0,    0,    0,    0,  532,    0,    0,    0,    0,
            0,    0,    0,    3,    0,  374,    0,    0,    0, -376,
            9,  375,  287,  739,  525,  694,    0,  331,   32,   17,
          376,  302,  377,  314,  378,  315,  379,  319,  350,  380,
          432,  332,  328,   33,   34,  381,  705,  374,   35,  382,
          648,  693,   60,  375,  808,  280,  281,  694,  552,  551,
           32,   17,  376,  385,  377,   31,  378,  647,  379,  701,
           30,  380,  646,  807,  810,   33,   34,  381,  806,  735,
           35,  382,  737,  683,   60,  747,  746,  280,  281,  740,
          755,  685,  707,  525,  696,  385,  690,  702,  689,  688,
           27,   97,   98,   99,  100,  101,  102,  103,  104,  105,
          106,  107,  108,  809,  917,  257,  374,  256,   69,  549,
          548,  546,  375,  544,  707,  525,  694,  541,  540,   32,
           17,  376,  536,  377,  535,  378,  533,  379,  526,  329,
          380,  854,  856,  916,   33,   34,  381,  719,  712,   35,
          382, 1025, -416,   60,  815,  645,  280,  281, 1026,  721,
          653,  652,  720,  918,  385,  744,  655,  654,  722,  545,
          681, 1023,  986,  979,  991,  996,  999,  745,  644,    0,
           36, -441,  339,  334,  266,  234,  233,  232,  231,  213,
          212,  210,  129,  707,  525, -421,  910,  127, -420, -419,
          120,   20,   23,   68,   67,   29,   62,   64,   66,   65,
         -443,    0,   15, -416,   19,  242,  894,  289,  456, -213,
          473,  893,  461,  518,  897,   11,  947, -416,  939,  515,
         -212,  371,  367,  365, -416,  363,   14,  964,   13,   12,
            0, -387,    0,  483,  990, 1021,  977,  978,  948
    );

    protected $actionCheck = array(
            2,    3,    4,    5,    6,    1,    8,    9,   10,   11,
           12,    8,    9,   10,   41,   42,   43,   44,   45,   46,
           47,   48,   49,   77,    8,    9,   10,    8,    9,   10,
            0,   28,   13,   30,   31,   32,   33,   34,   35,   36,
           37,   38,   39,   40,   28,    7,   30,   31,   32,   33,
           34,   35,   54,    8,    8,    9,   10,    7,  112,  113,
          114,  115,  116,  117,    8,    9,   68,   69,   70,   71,
           72,   73,   74,    7,   28,   77,   30,   31,   32,   33,
           34,    7,   84,   85,   86,   87,   88,   89,   90,   91,
           92,   93,   94,   95,   96,   97,   98,   99,  100,  101,
          102,  103,  104,  105,  106,  107,  108,  109,  110,  111,
          112,  113,  114,  115,  116,  117,  118,  119,  120,  121,
          122,  123,  124,  125,  126,  127,    7,  129,  130,  131,
          132,  133,  134,  135,  136,  137,    2,    3,    4,    5,
            6,  143,  144,  145,  103,   11,   12,    7,   14,   79,
          109,   67,  148,  112,  113,  114,  115,  116,  117,  118,
            7,   67,   50,   51,   52,   77,   54,   79,    7,   67,
          102,  103,   77,  103,   79,   80,   67,   83,   66,  109,
           79,   47,   48,   77,    7,   83,   35,   53,  118,   55,
           56,   57,   58,   59,   60,   61,   62,   63,   64,   65,
          112,   67,   68,   69,   70,    8,    9,   10,    7,   75,
           76,   77,  128,   79,  146,    7,    7,   83,    8,    9,
           10,    1,    7,  129,  130,   28,  142,   30,   31,   32,
           33,  143,  130,  149,  146,  151,  102,  128,   28,   13,
           30,   31,   32,   29,  156,  151,  112,   13,    7,  143,
          149,  142,  146,  151,  120,  121,  150,  156,  149,   15,
          151,    7,   35,  129,  130,   67,  132,  133,  134,  135,
          136,  137,  138,    8,    9,   10,    7,  143,  144,  145,
          146,   15,    8,    9,   10,  151,    7,  153,  148,  155,
          156,   71,   72,   73,   74,    0,    1,   77,  147,    7,
           67,   81,   28,  152,   84,   85,   86,   87,   88,   89,
           90,   91,   92,   93,   79,   95,   96,   97,   98,   99,
          100,  101,  102,   29,  104,  105,  128,   79,  108,  152,
           82,  111,  112,  113,  114,    8,    9,   10,   79,  119,
          142,    7,  122,  123,  124,  125,    7,  149,   47,   48,
           49,   67,   67,  152,    7,   28,   67,   30,   31,   79,
          152,  128,  154,  149,   29,    7,   71,  152,  148,  149,
          150,  112,   77,    7,  147,  142,   81,   15,  152,   84,
           85,   86,  149,   88,   35,   90,   15,   92,  140,  141,
           95,  156,  112,  152,   99,  100,  101,  102,  103,  104,
          105,   77,  148,  108,  109,    1,  111,  112,  113,  114,
          130,  131,  128,  128,  119,  156,   67,  122,  123,  124,
          125,  152,   15,  154,   15,   82,    8,    9,   10,   72,
           73,  152,   83,   29,  149,   29,  156,   15,  143,  150,
          148,   29,  148,  148,  149,  150,   28,   54,   30,   31,
           32,   33,   34,   35,   36,   37,   38,   39,   40,   41,
           42,   43,   44,   45,   46,   47,   48,   49,   50,   51,
           52,   29,   54,  149,    1,   71,   29,  146,  129,  130,
          149,   77,  139,   29,   66,   81,  152,   79,   84,   85,
           86,  152,   88,   35,   90,  148,   92,   72,   73,   95,
          151,   35,   29,   99,  100,  101,    1,   35,  104,  105,
          152,   35,  108,   67,  148,  111,  112,   97,   98,   66,
          112,  102,  103,  119,  148,  149,   74,  106,  107,  148,
          149,  148,  149,   77,   29,   78,   77,   77,  130,  131,
           77,   77,   77,   82,   71,   77,   77,   77,   77,   82,
           77,   77,  148,  149,   81,   93,   79,   84,   85,   86,
          152,   88,   79,   90,  156,   92,   79,   79,   95,   79,
           86,   89,   99,  100,  101,    1,   71,  104,  105,   87,
           91,  108,   77,  102,  111,  112,   81,   94,   94,   84,
           85,   86,  119,   88,   94,   90,   94,   92,  102,   96,
           95,  110,   96,   29,   99,  100,  101,    1,  109,  104,
          105,  126,  126,  108,  139,   -1,  111,  112,  123,  139,
          123,  148,  149,   -1,  119,  127,  147,  123,  150,   -1,
           -1,   -1,   -1,   -1,   -1,   29,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,  142,   -1,   71,   -1,   -1,   -1,  142,
          142,   77,  142,  148,  149,   81,   -1,  146,   84,   85,
           86,  146,   88,  146,   90,  146,   92,  146,  146,   95,
          146,  146,  149,   99,  100,  101,    1,   71,  104,  105,
          148,  148,  108,   77,  148,  111,  112,   81,  148,  148,
           84,   85,   86,  119,   88,  148,   90,  148,   92,  148,
          148,   95,  148,  148,  148,   99,  100,  101,  148,  148,
          104,  105,  148,  148,  108,  148,  148,  111,  112,  148,
          148,  148,  148,  149,  148,  119,  148,  148,  148,  148,
           15,   16,   17,   18,   19,   20,   21,   22,   23,   24,
           25,   26,   27,  148,  150,  149,   71,  149,  149,  149,
          149,  149,   77,  149,  148,  149,   81,  149,  149,   84,
           85,   86,  149,   88,  149,   90,  149,   92,  149,  149,
           95,   56,   57,  150,   99,  100,  101,  150,  150,  104,
          105,  150,   67,  108,  150,  150,  111,  112,  150,  150,
          150,  150,  150,  150,  119,  150,  150,  150,  150,  150,
          150,  150,  150,  150,  150,  150,  150,  150,  150,   -1,
          151,  151,  151,  151,  151,  151,  151,  151,  151,  151,
          151,  151,  151,  148,  149,  151,  153,  151,  151,  151,
          151,  151,  151,  151,  151,  151,  151,  151,  151,  151,
          151,   -1,  152,  128,  152,  152,  152,  152,  152,  152,
          152,  152,  152,  152,  152,  152,  152,  142,  152,  152,
          152,  152,  152,  152,  149,  152,  152,  155,  152,  152,
           -1,  153,   -1,  154,  154,  154,  154,  154,  154
    );

    protected $actionBase = array(
            0,  220,  295,  101,  106,  536,   -2,   -2,   -2,   -2,
          -54,  473,  606,  574,  606,  404,  505,  675,  675,  675,
          151,  227,  458,  458,  458,  457,  442,  476,  466,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          294,    4,  234,  551,  693,  702,  696,  690,  703,  494,
          695,  694,  651,  652,  406,  653,  654,  655,  656,  698,
          719,  692,  701,  418,  418,  418,  418,  418,  418,  418,
          418,  418,  418,  418,  418,  418,  418,  418,  418,   45,
           19,   56,  265,  265,  265,  265,  265,  265,  265,  265,
          265,  265,  265,  265,  265,  265,  265,  265,  265,  265,
          274,  274,  274,  327,  210,  197,   46,  715,   16,    3,
            3,    3,    3,    3,  -27,  -27,  -27,  -27,  349,  349,
           94,   94,  102,  102,  102,  102,  102,  102,  102,  102,
          102,  102,  102,  648,  639,  642,  643,  301,  301,  497,
           70,  408,  408,  408,  408,   88,  280,  343,  280,  475,
          712,   84,  109,  112,  112,  112,   68,  461,  248,  248,
          324,  324,  233,  233,  198,  233,  419,  419,  419,  259,
          259,  259,  259,  331,  259,  259,  259,  599,  467,   95,
          506,  645,  376,  503,  657,  285,  269,  208,  511,  525,
          235,  481,  235,  421,  425,  357,  507,  235,  235,  214,
          294,  381,  393,  533,  456,  366,  554,  292,  347,  284,
          383,  241,  598,  549,  700,  358,  699,  201,  279,  289,
          393,  393,  393,  334,  596,  523,  177,  226,  647,  620,
          215,  646,  641,  140,  254,  640,  339,  560,  471,  471,
          471,  471,  471,  471,  472,  471,  463,  680,  680,  459,
          490,  472,  659,  472,  471,  680,  472,  119,  472,  485,
          471,  486,  486,  463,  477,  498,  680,  680,  498,  459,
          472,  541,  540,  499,  479,  447,  447,  499,  472,  447,
          490,  447,   30,  685,  686,  454,  688,  684,  687,  661,
          683,  464,  619,  504,  495,  669,  668,  682,  460,  468,
          670,  681,  524,  532,  465,  422,  501,  446,  678,  481,
          522,  453,  453,  453,  446,  674,  453,  453,  453,  453,
          453,  453,  453,  453,  724,  510,  484,  581,  580,  579,
          409,  578,  515,  500,  407,  604,  480,  524,  524,  650,
          718,  673,  469,  667,  706,  679,  552,  153,  371,  666,
          649,  517,  470,  519,  665,  602,  704,  474,  638,  524,
          635,  453,  660,  689,  722,  723,  677,  720,  713,  161,
          521,  576,   66,  721,  658,  601,  600,  547,  717,  711,
          710,  496,   66,  573,  492,  697,  462,  662,  488,  663,
          617,  362,  266,  631,  676,  572,  716,  714,  708,  571,
          568,  615,  613,  244,  671,  335,  452,  489,  567,  487,
          483,  628,  609,  664,  565,  564,  627,  623,  707,  493,
          522,  508,  491,  502,  482,  608,  594,  709,  412,  561,
          595,  556,  478,  555,  634,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,  134,  134,   -2,   -2,   -2,
            0,    0,    0,    0,   -2,  134,  134,  134,  134,  134,
          134,  134,  134,  134,  134,  134,  134,  134,  134,  134,
          134,  134,  134,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,  418,  418,  418,  418,  418,  418,
          418,  418,  418,  418,  418,  418,  418,  418,  418,  418,
          418,  418,  418,  418,  418,  418,  418,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,  418,  418,  418,  418,  418,  418,  418,
          418,  418,  418,  418,  418,  418,  418,  418,  418,  418,
          418,  418,  418,  418,  418,  418,  418,  418,  418,  418,
          418,    0,  418,  418,  418,  418,  418,  418,  112,  112,
          112,  112,   88,   88,   88,   88,   88,   88,   88,   88,
           88,   88,   88,   88,   88,   88,   88,   41,   41,   41,
           41,  112,  112,   88,   41,   88,   88,   88,   88,    0,
           88,  248,   88,  248,  248,    0,    0,    0,    0,    0,
          471,  248,    0,    0,  235,  235,    0,    0,    0,    0,
          471,  471,  471,   88,   88,   88,   88,  471,   88,   88,
           88,  235,  248,    0,  420,  420,   66,  420,  420,    0,
            0,    0,  471,  471,    0,  477,    0,    0,    0,    0,
          680,    0,    0,    0,    0,    0,  453,  153,  667,    0,
           50,    0,    0,    0,    0,    0,  469,   50,  209,    0,
          209,    0,    0,    0,  453,  453,  453,    0,  469,  469,
            0,    0,   74,  469,    0,   74,   38,    0,    0,   38,
            0,   66
    );

    protected $actionDefault = array(
            3,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,  453,  453,  413,32767,32767,32767,32767,  280,
          280,  280,32767,  414,  414,  414,  414,  414,  414,  414,
        32767,32767,32767,32767,32767,  358,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,  458,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,  341,  342,  344,  345,  279,  415,  232,
          457,  278,  116,  241,  234,  189,  119,  277,  220,  306,
          359,  308,  357,  361,  307,  284,  288,  289,  290,  291,
          292,  293,  294,  295,  296,  297,  298,  299,  283,  360,
          338,  337,  336,  304,  305,  309,  311,  282,  310,  327,
          328,  325,  326,  329,  330,  331,  332,  333,32767,32767,
          452,  452,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,  264,  264,  264,  264,  318,  319,32767,
          265,  224,  224,  224,  224,32767,  224,32767,32767,32767,
        32767,  406,  335,  313,  314,  312,32767,  386,32767,  388,
        32767,32767,  301,  303,  381,  285,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,  383,  416,  416,32767,32767,
        32767,  375,32767,  157,  208,  210,  391,32767,32767,32767,
        32767,32767,  323,32767,32767,32767,32767,32767,32767,  466,
        32767,32767,32767,32767,32767,  416,32767,  416,32767,32767,
          315,  316,  317,32767,32767,32767,  416,  416,32767,32767,
          416,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,  161,32767,32767,  389,  389,32767,
        32767,  161,  384,  161,32767,32767,  161,  412,  161,  174,
        32767,  172,  172,32767,32767,  176,32767,  430,  176,32767,
          161,  194,  194,  367,  163,  226,  226,  367,  161,  226,
        32767,  226,32767,32767,32767,   82,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,  377,32767,32767,32767,32767,  407,  428,  375,
        32767,  321,  322,  324,32767,  418,  346,  347,  348,  349,
          350,  351,  352,  354,32767,  380,32767,32767,32767,32767,
        32767,32767,   84,  108,  240,32767,  465,   84,  378,32767,
          465,32767,32767,32767,32767,32767,32767,  281,32767,32767,
        32767,   84,32767,   84,32767,32767,32767,32767,  416,  379,
        32767,  320,  392,  434,32767,32767,  417,32767,32767,  215,
           84,32767,  175,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,  177,32767,32767,  416,32767,32767,32767,32767,
        32767,32767,  276,32767,32767,32767,32767,32767,  416,32767,
        32767,32767,32767,  219,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,32767,32767,32767,32767,   82,
           60,32767,  258,32767,32767,32767,32767,32767,32767,32767,
        32767,32767,32767,32767,32767,  121,  121,    3,    3,  121,
          121,  121,  121,  121,  121,  121,  121,  121,  121,  121,
          121,  121,  121,  121,  243,  154,  243,  202,  243,  243,
          205,  194,  194,  250
    );

    protected $goto = array(
          159,  159,  132,  132,  132,  142,  144,  175,  160,  157,
          157,  157,  157,  158,  158,  158,  158,  158,  158,  158,
          153,  154,  155,  156,  172,  170,  173,  401,  402,  292,
          403,  406,  407,  408,  409,  410,  411,  412,  413,  841,
          133,  134,  135,  136,  137,  138,  139,  140,  141,  143,
          169,  171,  174,  190,  193,  194,  195,  196,  198,  199,
          200,  201,  202,  203,  204,  205,  206,  207,  227,  228,
          243,  244,  245,  310,  311,  312,  451,  176,  177,  178,
          179,  180,  181,  182,  183,  184,  185,  186,  187,  188,
          145,  189,  146,  161,  162,  163,  191,  164,  147,  148,
          149,  165,  150,  192,  130,  166,  167,  151,  168,  152,
          511,  422,  452,  813,  523,  677,  639,  503,  811,  271,
          641,  427,  427,  427,  640,  754,  453,  734,  427,  547,
            5,  416,  763,  758,  418,  421,  434,  454,  455,  457,
          467,  486,  440,  443,  427,  550,  474,  476,  497,  501,
          751,  506,  507,  765,  514,  750,  516,  522,  761,  524,
          317,  488,  307,  307,  305,  305,  252,  253,  276,  448,
          255,  316,  277,  320,  475,  404,  404,  404,  404,  404,
          404,  404,  404,  404,  404,  404,  404,  404,  404,  404,
          926,  249,  240,  427,  427,  441,  460,  427,  427,  664,
          427,  768,  768, 1005, 1005,  492,  415,  671,  502,  428,
          291,  224,  415,  225,  226,  449,  405,  405,  405,  405,
          405,  405,  405,  405,  405,  405,  405,  405,  405,  405,
          405,  664,  664,  294, 1015, 1015,  433,  521,  444,  450,
          464, 1016, 1016,  698, 1015,  469,  470,  517,  738,  927,
          364, 1016,  472,  272,  327,  785,  446,  293,    8, 1009,
          928,  981,  487, 1018,  306, 1002,  888,  781,  772,  278,
          992,  321,  300,  660,  303,  534,  658,  325,  919,  924,
          789,  657,  657,  665,  665,  665,  667,  358,  656,  668,
          466,  792,  742,  369,  829,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,  273,  274,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,  930,    0,  775,  775,  775,
          775,  930,  775,    0,  775,    0,    0,    0,    0,    0,
          821,    0,  989,    0,    0,    0,    0,    0,  989,    0,
            0,    0,    0,    0,    0,  732,  732,  732,  732,    0,
          727,  733,  500, 1000, 1000,    0,    0,    0,    0,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,    0,
          987,    0,    0,    0,    0,    0,    0,    0,    0,    0,
            0,    0,    0,  791,    0,  791,    0,    0,    0,    0,
          993,  994
    );

    protected $gotoCheck = array(
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           51,    8,    7,    7,    7,   10,   10,    7,    7,   63,
           12,    8,    8,    8,   11,   10,   77,   10,    8,   10,
           89,   10,   10,   10,   38,   38,   38,   38,   38,   38,
           35,   35,   28,    8,    8,   28,   28,   28,   28,   28,
           28,   28,   28,   28,   28,   28,   28,   28,   28,   28,
           45,   45,   45,   45,   45,   45,   45,   45,   45,   45,
           45,   45,   45,   45,   45,  110,  110,  110,  110,  110,
          110,  110,  110,  110,  110,  110,  110,  110,  110,  110,
           73,  109,  109,    8,    8,    8,    8,    8,    8,   19,
            8,   68,   68,   68,   68,   55,  106,   25,   55,    8,
           55,   59,  106,   59,   59,    8,  111,  111,  111,  111,
          111,  111,  111,  111,  111,  111,  111,  111,  111,  111,
          111,   19,   19,   52,  121,  121,   52,    5,   52,    2,
            2,  122,  122,   44,  121,   54,   54,   54,   29,   73,
           52,  122,   61,   61,   61,   75,  112,   41,   52,  120,
           73,   73,   43,  121,   42,  118,   93,   72,   70,   14,
          115,   18,    9,   21,   13,   65,   20,   17,   99,  101,
           76,   19,   19,   19,   19,   19,   19,   57,   19,   22,
           58,   78,   62,   97,   91,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   63,   63,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   51,   -1,   51,   51,   51,
           51,   51,   51,   -1,   51,   -1,   -1,   -1,   -1,   -1,
           89,   -1,   77,   -1,   -1,   -1,   -1,   -1,   77,   -1,
           -1,   -1,   -1,   -1,   -1,   51,   51,   51,   51,   -1,
           51,   51,   51,   77,   77,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           77,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,   -1,
           -1,   -1,   -1,   77,   -1,   77,   -1,   -1,   -1,   -1,
           77,   77
    );

    protected $gotoBase = array(
            0,    0, -288,    0,    0,  227,    0,  109, -135,    9,
          114,  122,  118,   -4,   23,    0,    0,  -52,   14,  -47,
           -3,   15,  -64,  -20,    0,  200,    0,    0, -384,  232,
            0,    0,    0,    0,    0,  110,    0,    0,  100,    0,
            0,  225,   51,   53,  229,  -48,    0,    0,    0,    0,
            0,  106, -110,    0,   13, -161,    0,  -65,  -68, -335,
            0,   -8,  -67, -243,    0,  -15,    0,    0,   -7,    0,
           32,    0,   29,  -96,    0,  234,   -2,  123,  -63,    0,
            0,    0,    0,    0,    0,    0,    0,    0,    0,  120,
            0,  -76,    0,   31,    0,    0,    0,  -74,    0,  -60,
            0,  -62,    0,    0,    0,    0,  -23,    0,    0,  -56,
          -33,    8,  233,    0,    0,   19,    0,    0,   54,    0,
          235,   -5,    2,    0
    );

    protected $gotoDefault = array(
        -32768,  372,  555,    2,  556,  627,  635,  481,  392,  423,
          736,  678,  679,  296,  333,  393,  295,  322,  318,  666,
          659,  661,  669,  131,  323,  672,    1,  674,  429,  706,
          284,  682,  285,  496,  684,  436,  686,  687,  417,  297,
          298,  437,  304,  468,  697,  197,  301,  699,  283,  700,
          709,  286,  499,  478,  458,  491,  394,  355,  465,  223,
          445,  462,  741,  270,  749,  539,  757,  760,  395,  459,
          771,  360,  779,  944,  313,  784,  790,  976,  793,  796,
          340,  324,  471,  800,  801,    4,  805,  512,  513,  820,
          230,  828,  840,  338,  907,  909,  431,  366,  920,  352,
          326,  923,  980,  345,  396,  356,  936,  254,  275,  239,
          397,  241,  414, 1008,  398,  357,  983,  308, 1003,  347,
         1010, 1017,  268,  463
    );

    protected $ruleToNonTerminal = array(
            0,    1,    3,    3,    2,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    5,    5,    5,    5,    5,    5,    5,
            5,    5,    5,    6,    6,    6,    6,    6,    6,    6,
            7,    7,    8,    8,    9,    4,    4,    4,    4,    4,
            4,    4,    4,    4,    4,    4,   14,   14,   15,   15,
           15,   15,   17,   17,   13,   13,   18,   18,   19,   19,
           20,   20,   21,   21,   16,   16,   22,   24,   24,   25,
           26,   26,   28,   27,   27,   27,   27,   29,   29,   29,
           29,   29,   29,   29,   29,   29,   29,   29,   29,   29,
           29,   29,   29,   29,   29,   29,   29,   29,   29,   29,
           29,   29,   10,   10,   48,   48,   50,   49,   49,   42,
           42,   52,   52,   53,   53,   11,   12,   12,   12,   56,
           56,   56,   57,   57,   60,   60,   58,   58,   61,   61,
           36,   36,   44,   44,   47,   47,   47,   46,   46,   62,
           37,   37,   37,   37,   63,   63,   64,   64,   65,   65,
           34,   34,   30,   30,   66,   32,   32,   67,   31,   31,
           33,   33,   43,   43,   43,   54,   54,   69,   69,   70,
           70,   72,   72,   72,   71,   71,   55,   55,   73,   73,
           74,   74,   75,   75,   75,   39,   39,   76,   40,   40,
           78,   78,   59,   59,   79,   79,   79,   79,   84,   84,
           85,   85,   86,   86,   86,   86,   86,   87,   88,   88,
           83,   83,   80,   80,   82,   82,   90,   90,   89,   89,
           89,   89,   89,   89,   81,   81,   91,   91,   41,   41,
           35,   35,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   23,   23,   23,   23,   23,   23,
           23,   23,   23,   23,   98,   92,   92,   97,   97,  100,
          100,  101,  102,  102,  102,  106,  106,   51,   51,   51,
           93,   93,  104,  104,   94,   94,   96,   96,   96,   99,
           99,  110,  110,  111,  111,  111,   95,   95,   95,   95,
           95,   95,   95,   95,   95,   95,   95,   95,   95,   95,
           95,   95,  113,  113,   38,   38,  108,  108,  108,  103,
          103,  103,  114,  114,  114,  114,  114,  114,   45,   45,
           45,   77,   77,   77,  116,  107,  107,  107,  107,  107,
          107,  105,  105,  105,  115,  115,  115,   68,  117,  117,
          118,  118,  118,  112,  112,  119,  119,  120,  120,  120,
          120,  109,  109,  109,  109,  122,  121,  121,  121,  121,
          121,  121,  121,  123,  123,  123
    );

    protected $ruleToLength = array(
            1,    1,    2,    0,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    1,    1,
            1,    1,    1,    3,    1,    1,    1,    1,    1,    3,
            5,    4,    3,    4,    2,    3,    1,    1,    7,    8,
            6,    7,    3,    1,    3,    1,    3,    1,    1,    3,
            1,    2,    1,    2,    3,    1,    3,    3,    1,    3,
            2,    0,    1,    1,    1,    1,    1,    3,    7,   10,
            5,    7,    9,    5,    3,    3,    3,    3,    3,    3,
            1,    2,    5,    7,    9,    5,    6,    3,    3,    2,
            2,    1,    1,    1,    0,    2,    8,    0,    4,    1,
            3,    0,    1,    0,    1,   10,    7,    6,    5,    1,
            2,    2,    0,    2,    0,    2,    0,    2,    1,    3,
            1,    4,    1,    4,    1,    1,    4,    1,    3,    3,
            3,    4,    4,    5,    0,    2,    4,    3,    1,    1,
            1,    4,    0,    2,    5,    0,    2,    6,    0,    2,
            0,    3,    1,    2,    1,    1,    0,    1,    3,    4,
            6,    1,    1,    1,    0,    1,    0,    2,    2,    3,
            1,    3,    1,    2,    2,    3,    1,    1,    3,    1,
            1,    3,    2,    0,    3,    3,    9,    3,    1,    3,
            0,    2,    4,    5,    4,    4,    4,    3,    1,    1,
            1,    3,    1,    1,    0,    1,    1,    2,    1,    1,
            1,    1,    1,    1,    1,    3,    1,    3,    3,    1,
            0,    1,    1,    3,    3,    4,    1,    2,    3,    3,
            3,    3,    3,    3,    3,    3,    3,    3,    3,    3,
            2,    2,    2,    2,    3,    3,    3,    3,    3,    3,
            3,    3,    3,    3,    3,    3,    3,    3,    3,    3,
            3,    2,    2,    2,    2,    3,    3,    3,    3,    3,
            3,    3,    3,    3,    3,    3,    5,    4,    3,    4,
            4,    2,    2,    4,    2,    2,    2,    2,    2,    2,
            2,    2,    2,    2,    2,    1,    3,    2,    1,    2,
            4,    2,   10,   11,    7,    3,    2,    0,    4,    1,
            3,    2,    2,    2,    4,    1,    1,    1,    2,    3,
            1,    1,    1,    1,    0,    3,    0,    1,    1,    0,
            1,    1,    3,    4,    3,    1,    1,    1,    1,    1,
            1,    1,    1,    1,    1,    1,    1,    1,    3,    2,
            3,    3,    0,    1,    0,    1,    1,    3,    1,    1,
            3,    1,    1,    4,    4,    4,    1,    4,    1,    1,
            3,    1,    4,    2,    3,    1,    4,    4,    3,    3,
            3,    1,    3,    1,    1,    3,    1,    4,    3,    1,
            1,    1,    0,    0,    2,    3,    1,    3,    1,    4,
            2,    2,    2,    1,    2,    1,    1,    4,    3,    3,
            3,    6,    3,    1,    1,    1
    );

    protected function reduceRule0() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule1() {
         $this->semValue = $this->handleNamespaces($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule2() {
         if (is_array($this->semStack[$this->stackPos-(2-2)])) { $this->semValue = array_merge($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)]); } else { $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)]; };
    }

    protected function reduceRule3() {
         $this->semValue = array();
    }

    protected function reduceRule4() {
         $startAttributes = $this->lookaheadStartAttributes; if (isset($startAttributes['comments'])) { $nop = new Stmt\Nop(['comments' => $startAttributes['comments']]); } else { $nop = null; };
            if ($nop !== null) { $this->semStack[$this->stackPos-(1-1)][] = $nop; } $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule5() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule6() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule7() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule8() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule9() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule10() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule11() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule12() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule13() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule14() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule15() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule16() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule17() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule18() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule19() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule20() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule21() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule22() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule23() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule24() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule25() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule26() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule27() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule28() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule29() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule30() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule31() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule32() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule33() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule34() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule35() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule36() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule37() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule38() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule39() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule40() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule41() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule42() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule43() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule44() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule45() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule46() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule47() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule48() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule49() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule50() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule51() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule52() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule53() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule54() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule55() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule56() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule57() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule58() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule59() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule60() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule61() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule62() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule63() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule64() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule65() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule66() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule67() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule68() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule69() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule70() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule71() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule72() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule73() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule74() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule75() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule76() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule77() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule78() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule79() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule80() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule81() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule82() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule83() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule84() {
         $this->semValue = new Name($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule85() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule86() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule87() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule88() {
         $this->semValue = new Stmt\HaltCompiler($this->lexer->handleHaltCompiler(), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule89() {
         $this->semValue = new Stmt\Namespace_($this->semStack[$this->stackPos-(3-2)], null, $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule90() {
         $this->semValue = new Stmt\Namespace_($this->semStack[$this->stackPos-(5-2)], $this->semStack[$this->stackPos-(5-4)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule91() {
         $this->semValue = new Stmt\Namespace_(null, $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule92() {
         $this->semValue = new Stmt\Use_($this->semStack[$this->stackPos-(3-2)], Stmt\Use_::TYPE_NORMAL, $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule93() {
         $this->semValue = new Stmt\Use_($this->semStack[$this->stackPos-(4-3)], $this->semStack[$this->stackPos-(4-2)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule94() {
         $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule95() {
         $this->semValue = new Stmt\Const_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule96() {
         $this->semValue = Stmt\Use_::TYPE_FUNCTION;
    }

    protected function reduceRule97() {
         $this->semValue = Stmt\Use_::TYPE_CONSTANT;
    }

    protected function reduceRule98() {
         $this->semValue = new Stmt\GroupUse(new Name($this->semStack[$this->stackPos-(7-3)], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(7-6)], $this->semStack[$this->stackPos-(7-2)], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes);
    }

    protected function reduceRule99() {
         $this->semValue = new Stmt\GroupUse(new Name($this->semStack[$this->stackPos-(8-4)], $this->startAttributeStack[$this->stackPos-(8-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(8-7)], $this->semStack[$this->stackPos-(8-2)], $this->startAttributeStack[$this->stackPos-(8-1)] + $this->endAttributes);
    }

    protected function reduceRule100() {
         $this->semValue = new Stmt\GroupUse(new Name($this->semStack[$this->stackPos-(6-2)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(6-5)], Stmt\Use_::TYPE_UNKNOWN, $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule101() {
         $this->semValue = new Stmt\GroupUse(new Name($this->semStack[$this->stackPos-(7-3)], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(7-6)], Stmt\Use_::TYPE_UNKNOWN, $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes);
    }

    protected function reduceRule102() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule103() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule104() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule105() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule106() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule107() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule108() {
         $this->semValue = new Stmt\UseUse($this->semStack[$this->stackPos-(1-1)], null, Stmt\Use_::TYPE_UNKNOWN, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule109() {
         $this->semValue = new Stmt\UseUse($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], Stmt\Use_::TYPE_UNKNOWN, $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule110() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule111() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule112() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)]; $this->semValue->type = Stmt\Use_::TYPE_NORMAL;
    }

    protected function reduceRule113() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)]; $this->semValue->type = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule114() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule115() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule116() {
         $this->semValue = new Node\Const_($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule117() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule118() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule119() {
         $this->semValue = new Node\Const_($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule120() {
         if (is_array($this->semStack[$this->stackPos-(2-2)])) { $this->semValue = array_merge($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)]); } else { $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)]; };
    }

    protected function reduceRule121() {
         $this->semValue = array();
    }

    protected function reduceRule122() {
         $startAttributes = $this->lookaheadStartAttributes; if (isset($startAttributes['comments'])) { $nop = new Stmt\Nop(['comments' => $startAttributes['comments']]); } else { $nop = null; };
            if ($nop !== null) { $this->semStack[$this->stackPos-(1-1)][] = $nop; } $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule123() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule124() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule125() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule126() {
         throw new Error('__HALT_COMPILER() can only be used from the outermost scope', $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule127() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)]; $attrs = $this->startAttributeStack[$this->stackPos-(3-1)]; $stmts = $this->semValue; if (!empty($attrs['comments']) && isset($stmts[0])) {$stmts[0]->setAttribute('comments', array_merge($attrs['comments'], $stmts[0]->getAttribute('comments', []))); };
    }

    protected function reduceRule128() {
         $this->semValue = new Stmt\If_($this->semStack[$this->stackPos-(7-3)], ['stmts' => is_array($this->semStack[$this->stackPos-(7-5)]) ? $this->semStack[$this->stackPos-(7-5)] : array($this->semStack[$this->stackPos-(7-5)]), 'elseifs' => $this->semStack[$this->stackPos-(7-6)], 'else' => $this->semStack[$this->stackPos-(7-7)]], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes);
    }

    protected function reduceRule129() {
         $this->semValue = new Stmt\If_($this->semStack[$this->stackPos-(10-3)], ['stmts' => $this->semStack[$this->stackPos-(10-6)], 'elseifs' => $this->semStack[$this->stackPos-(10-7)], 'else' => $this->semStack[$this->stackPos-(10-8)]], $this->startAttributeStack[$this->stackPos-(10-1)] + $this->endAttributes);
    }

    protected function reduceRule130() {
         $this->semValue = new Stmt\While_($this->semStack[$this->stackPos-(5-3)], $this->semStack[$this->stackPos-(5-5)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule131() {
         $this->semValue = new Stmt\Do_($this->semStack[$this->stackPos-(7-5)], is_array($this->semStack[$this->stackPos-(7-2)]) ? $this->semStack[$this->stackPos-(7-2)] : array($this->semStack[$this->stackPos-(7-2)]), $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes);
    }

    protected function reduceRule132() {
         $this->semValue = new Stmt\For_(['init' => $this->semStack[$this->stackPos-(9-3)], 'cond' => $this->semStack[$this->stackPos-(9-5)], 'loop' => $this->semStack[$this->stackPos-(9-7)], 'stmts' => $this->semStack[$this->stackPos-(9-9)]], $this->startAttributeStack[$this->stackPos-(9-1)] + $this->endAttributes);
    }

    protected function reduceRule133() {
         $this->semValue = new Stmt\Switch_($this->semStack[$this->stackPos-(5-3)], $this->semStack[$this->stackPos-(5-5)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule134() {
         $this->semValue = new Stmt\Break_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule135() {
         $this->semValue = new Stmt\Continue_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule136() {
         $this->semValue = new Stmt\Return_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule137() {
         $this->semValue = new Stmt\Global_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule138() {
         $this->semValue = new Stmt\Static_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule139() {
         $this->semValue = new Stmt\Echo_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule140() {
         $this->semValue = new Stmt\InlineHTML($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule141() {
         $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule142() {
         $this->semValue = new Stmt\Unset_($this->semStack[$this->stackPos-(5-3)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule143() {
         $this->semValue = new Stmt\Foreach_($this->semStack[$this->stackPos-(7-3)], $this->semStack[$this->stackPos-(7-5)][0], ['keyVar' => null, 'byRef' => $this->semStack[$this->stackPos-(7-5)][1], 'stmts' => $this->semStack[$this->stackPos-(7-7)]], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes);
    }

    protected function reduceRule144() {
         $this->semValue = new Stmt\Foreach_($this->semStack[$this->stackPos-(9-3)], $this->semStack[$this->stackPos-(9-7)][0], ['keyVar' => $this->semStack[$this->stackPos-(9-5)], 'byRef' => $this->semStack[$this->stackPos-(9-7)][1], 'stmts' => $this->semStack[$this->stackPos-(9-9)]], $this->startAttributeStack[$this->stackPos-(9-1)] + $this->endAttributes);
    }

    protected function reduceRule145() {
         $this->semValue = new Stmt\Declare_($this->semStack[$this->stackPos-(5-3)], $this->semStack[$this->stackPos-(5-5)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule146() {
         $this->semValue = new Stmt\TryCatch($this->semStack[$this->stackPos-(6-3)], $this->semStack[$this->stackPos-(6-5)], $this->semStack[$this->stackPos-(6-6)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule147() {
         $this->semValue = new Stmt\Throw_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule148() {
         $this->semValue = new Stmt\Goto_($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule149() {
         $this->semValue = new Stmt\Label($this->semStack[$this->stackPos-(2-1)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule150() {
         $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule151() {
         $this->semValue = array(); /* means: no statement */
    }

    protected function reduceRule152() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule153() {
         $startAttributes = $this->startAttributeStack[$this->stackPos-(1-1)]; if (isset($startAttributes['comments'])) { $this->semValue = new Stmt\Nop(['comments' => $startAttributes['comments']]); } else { $this->semValue = null; };
            if ($this->semValue === null) $this->semValue = array(); /* means: no statement */
    }

    protected function reduceRule154() {
         $this->semValue = array();
    }

    protected function reduceRule155() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule156() {
         $this->semValue = new Stmt\Catch_($this->semStack[$this->stackPos-(8-3)], substr($this->semStack[$this->stackPos-(8-4)], 1), $this->semStack[$this->stackPos-(8-7)], $this->startAttributeStack[$this->stackPos-(8-1)] + $this->endAttributes);
    }

    protected function reduceRule157() {
         $this->semValue = null;
    }

    protected function reduceRule158() {
         $this->semValue = $this->semStack[$this->stackPos-(4-3)];
    }

    protected function reduceRule159() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule160() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule161() {
         $this->semValue = false;
    }

    protected function reduceRule162() {
         $this->semValue = true;
    }

    protected function reduceRule163() {
         $this->semValue = false;
    }

    protected function reduceRule164() {
         $this->semValue = true;
    }

    protected function reduceRule165() {
         $this->semValue = new Stmt\Function_($this->semStack[$this->stackPos-(10-3)], ['byRef' => $this->semStack[$this->stackPos-(10-2)], 'params' => $this->semStack[$this->stackPos-(10-5)], 'returnType' => $this->semStack[$this->stackPos-(10-7)], 'stmts' => $this->semStack[$this->stackPos-(10-9)]], $this->startAttributeStack[$this->stackPos-(10-1)] + $this->endAttributes);
    }

    protected function reduceRule166() {
         $this->semValue = new Stmt\Class_($this->semStack[$this->stackPos-(7-2)], ['type' => $this->semStack[$this->stackPos-(7-1)], 'extends' => $this->semStack[$this->stackPos-(7-3)], 'implements' => $this->semStack[$this->stackPos-(7-4)], 'stmts' => $this->semStack[$this->stackPos-(7-6)]], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes);
    }

    protected function reduceRule167() {
         $this->semValue = new Stmt\Interface_($this->semStack[$this->stackPos-(6-2)], ['extends' => $this->semStack[$this->stackPos-(6-3)], 'stmts' => $this->semStack[$this->stackPos-(6-5)]], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule168() {
         $this->semValue = new Stmt\Trait_($this->semStack[$this->stackPos-(5-2)], $this->semStack[$this->stackPos-(5-4)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule169() {
         $this->semValue = 0;
    }

    protected function reduceRule170() {
         $this->semValue = Stmt\Class_::MODIFIER_ABSTRACT;
    }

    protected function reduceRule171() {
         $this->semValue = Stmt\Class_::MODIFIER_FINAL;
    }

    protected function reduceRule172() {
         $this->semValue = null;
    }

    protected function reduceRule173() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule174() {
         $this->semValue = array();
    }

    protected function reduceRule175() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule176() {
         $this->semValue = array();
    }

    protected function reduceRule177() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule178() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule179() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule180() {
         $this->semValue = is_array($this->semStack[$this->stackPos-(1-1)]) ? $this->semStack[$this->stackPos-(1-1)] : array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule181() {
         $this->semValue = $this->semStack[$this->stackPos-(4-2)];
    }

    protected function reduceRule182() {
         $this->semValue = is_array($this->semStack[$this->stackPos-(1-1)]) ? $this->semStack[$this->stackPos-(1-1)] : array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule183() {
         $this->semValue = $this->semStack[$this->stackPos-(4-2)];
    }

    protected function reduceRule184() {
         $this->semValue = is_array($this->semStack[$this->stackPos-(1-1)]) ? $this->semStack[$this->stackPos-(1-1)] : array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule185() {
         $this->semValue = null;
    }

    protected function reduceRule186() {
         $this->semValue = $this->semStack[$this->stackPos-(4-2)];
    }

    protected function reduceRule187() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule188() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule189() {
         $this->semValue = new Stmt\DeclareDeclare($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule190() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule191() {
         $this->semValue = $this->semStack[$this->stackPos-(4-3)];
    }

    protected function reduceRule192() {
         $this->semValue = $this->semStack[$this->stackPos-(4-2)];
    }

    protected function reduceRule193() {
         $this->semValue = $this->semStack[$this->stackPos-(5-3)];
    }

    protected function reduceRule194() {
         $this->semValue = array();
    }

    protected function reduceRule195() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule196() {
         $this->semValue = new Stmt\Case_($this->semStack[$this->stackPos-(4-2)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule197() {
         $this->semValue = new Stmt\Case_(null, $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule198() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule199() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule200() {
         $this->semValue = is_array($this->semStack[$this->stackPos-(1-1)]) ? $this->semStack[$this->stackPos-(1-1)] : array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule201() {
         $this->semValue = $this->semStack[$this->stackPos-(4-2)];
    }

    protected function reduceRule202() {
         $this->semValue = array();
    }

    protected function reduceRule203() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule204() {
         $this->semValue = new Stmt\ElseIf_($this->semStack[$this->stackPos-(5-3)], is_array($this->semStack[$this->stackPos-(5-5)]) ? $this->semStack[$this->stackPos-(5-5)] : array($this->semStack[$this->stackPos-(5-5)]), $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule205() {
         $this->semValue = array();
    }

    protected function reduceRule206() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule207() {
         $this->semValue = new Stmt\ElseIf_($this->semStack[$this->stackPos-(6-3)], $this->semStack[$this->stackPos-(6-6)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule208() {
         $this->semValue = null;
    }

    protected function reduceRule209() {
         $this->semValue = new Stmt\Else_(is_array($this->semStack[$this->stackPos-(2-2)]) ? $this->semStack[$this->stackPos-(2-2)] : array($this->semStack[$this->stackPos-(2-2)]), $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule210() {
         $this->semValue = null;
    }

    protected function reduceRule211() {
         $this->semValue = new Stmt\Else_($this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule212() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)], false);
    }

    protected function reduceRule213() {
         $this->semValue = array($this->semStack[$this->stackPos-(2-2)], true);
    }

    protected function reduceRule214() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)], false);
    }

    protected function reduceRule215() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule216() {
         $this->semValue = array();
    }

    protected function reduceRule217() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule218() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule219() {
         $this->semValue = new Node\Param(substr($this->semStack[$this->stackPos-(4-4)], 1), null, $this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-2)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule220() {
         $this->semValue = new Node\Param(substr($this->semStack[$this->stackPos-(6-4)], 1), $this->semStack[$this->stackPos-(6-6)], $this->semStack[$this->stackPos-(6-1)], $this->semStack[$this->stackPos-(6-2)], $this->semStack[$this->stackPos-(6-3)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule221() {
         $this->semValue = $this->handleScalarTypes($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule222() {
         $this->semValue = 'array';
    }

    protected function reduceRule223() {
         $this->semValue = 'callable';
    }

    protected function reduceRule224() {
         $this->semValue = null;
    }

    protected function reduceRule225() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule226() {
         $this->semValue = null;
    }

    protected function reduceRule227() {
         $this->semValue = $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule228() {
         $this->semValue = array();
    }

    protected function reduceRule229() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule230() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule231() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule232() {
         $this->semValue = new Node\Arg($this->semStack[$this->stackPos-(1-1)], false, false, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule233() {
         $this->semValue = new Node\Arg($this->semStack[$this->stackPos-(2-2)], true, false, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule234() {
         $this->semValue = new Node\Arg($this->semStack[$this->stackPos-(2-2)], false, true, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule235() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule236() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule237() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule238() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule239() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule240() {
         $this->semValue = new Stmt\StaticVar(substr($this->semStack[$this->stackPos-(1-1)], 1), null, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule241() {
         $this->semValue = new Stmt\StaticVar(substr($this->semStack[$this->stackPos-(3-1)], 1), $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule242() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule243() {
         $this->semValue = array();
    }

    protected function reduceRule244() {
         $this->semValue = new Stmt\Property($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule245() {
         $this->semValue = new Stmt\ClassConst($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule246() {
         $this->semValue = new Stmt\ClassMethod($this->semStack[$this->stackPos-(9-4)], ['type' => $this->semStack[$this->stackPos-(9-1)], 'byRef' => $this->semStack[$this->stackPos-(9-3)], 'params' => $this->semStack[$this->stackPos-(9-6)], 'returnType' => $this->semStack[$this->stackPos-(9-8)], 'stmts' => $this->semStack[$this->stackPos-(9-9)]], $this->startAttributeStack[$this->stackPos-(9-1)] + $this->endAttributes);
    }

    protected function reduceRule247() {
         $this->semValue = new Stmt\TraitUse($this->semStack[$this->stackPos-(3-2)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule248() {
         $this->semValue = array();
    }

    protected function reduceRule249() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule250() {
         $this->semValue = array();
    }

    protected function reduceRule251() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule252() {
         $this->semValue = new Stmt\TraitUseAdaptation\Precedence($this->semStack[$this->stackPos-(4-1)][0], $this->semStack[$this->stackPos-(4-1)][1], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule253() {
         $this->semValue = new Stmt\TraitUseAdaptation\Alias($this->semStack[$this->stackPos-(5-1)][0], $this->semStack[$this->stackPos-(5-1)][1], $this->semStack[$this->stackPos-(5-3)], $this->semStack[$this->stackPos-(5-4)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule254() {
         $this->semValue = new Stmt\TraitUseAdaptation\Alias($this->semStack[$this->stackPos-(4-1)][0], $this->semStack[$this->stackPos-(4-1)][1], $this->semStack[$this->stackPos-(4-3)], null, $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule255() {
         $this->semValue = new Stmt\TraitUseAdaptation\Alias($this->semStack[$this->stackPos-(4-1)][0], $this->semStack[$this->stackPos-(4-1)][1], null, $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule256() {
         $this->semValue = new Stmt\TraitUseAdaptation\Alias($this->semStack[$this->stackPos-(4-1)][0], $this->semStack[$this->stackPos-(4-1)][1], null, $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule257() {
         $this->semValue = array($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)]);
    }

    protected function reduceRule258() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule259() {
         $this->semValue = array(null, $this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule260() {
         $this->semValue = null;
    }

    protected function reduceRule261() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule262() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule263() {
         $this->semValue = 0;
    }

    protected function reduceRule264() {
         $this->semValue = 0;
    }

    protected function reduceRule265() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule266() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule267() {
         Stmt\Class_::verifyModifier($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)]); $this->semValue = $this->semStack[$this->stackPos-(2-1)] | $this->semStack[$this->stackPos-(2-2)];
    }

    protected function reduceRule268() {
         $this->semValue = Stmt\Class_::MODIFIER_PUBLIC;
    }

    protected function reduceRule269() {
         $this->semValue = Stmt\Class_::MODIFIER_PROTECTED;
    }

    protected function reduceRule270() {
         $this->semValue = Stmt\Class_::MODIFIER_PRIVATE;
    }

    protected function reduceRule271() {
         $this->semValue = Stmt\Class_::MODIFIER_STATIC;
    }

    protected function reduceRule272() {
         $this->semValue = Stmt\Class_::MODIFIER_ABSTRACT;
    }

    protected function reduceRule273() {
         $this->semValue = Stmt\Class_::MODIFIER_FINAL;
    }

    protected function reduceRule274() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule275() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule276() {
         $this->semValue = new Stmt\PropertyProperty(substr($this->semStack[$this->stackPos-(1-1)], 1), null, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule277() {
         $this->semValue = new Stmt\PropertyProperty(substr($this->semStack[$this->stackPos-(3-1)], 1), $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule278() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule279() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule280() {
         $this->semValue = array();
    }

    protected function reduceRule281() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule282() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule283() {
         $this->semValue = new Expr\Assign($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule284() {
         $this->semValue = new Expr\Assign($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule285() {
         $this->semValue = new Expr\AssignRef($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule286() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule287() {
         $this->semValue = new Expr\Clone_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule288() {
         $this->semValue = new Expr\AssignOp\Plus($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule289() {
         $this->semValue = new Expr\AssignOp\Minus($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule290() {
         $this->semValue = new Expr\AssignOp\Mul($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule291() {
         $this->semValue = new Expr\AssignOp\Div($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule292() {
         $this->semValue = new Expr\AssignOp\Concat($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule293() {
         $this->semValue = new Expr\AssignOp\Mod($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule294() {
         $this->semValue = new Expr\AssignOp\BitwiseAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule295() {
         $this->semValue = new Expr\AssignOp\BitwiseOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule296() {
         $this->semValue = new Expr\AssignOp\BitwiseXor($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule297() {
         $this->semValue = new Expr\AssignOp\ShiftLeft($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule298() {
         $this->semValue = new Expr\AssignOp\ShiftRight($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule299() {
         $this->semValue = new Expr\AssignOp\Pow($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule300() {
         $this->semValue = new Expr\PostInc($this->semStack[$this->stackPos-(2-1)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule301() {
         $this->semValue = new Expr\PreInc($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule302() {
         $this->semValue = new Expr\PostDec($this->semStack[$this->stackPos-(2-1)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule303() {
         $this->semValue = new Expr\PreDec($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule304() {
         $this->semValue = new Expr\BinaryOp\BooleanOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule305() {
         $this->semValue = new Expr\BinaryOp\BooleanAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule306() {
         $this->semValue = new Expr\BinaryOp\LogicalOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule307() {
         $this->semValue = new Expr\BinaryOp\LogicalAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule308() {
         $this->semValue = new Expr\BinaryOp\LogicalXor($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule309() {
         $this->semValue = new Expr\BinaryOp\BitwiseOr($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule310() {
         $this->semValue = new Expr\BinaryOp\BitwiseAnd($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule311() {
         $this->semValue = new Expr\BinaryOp\BitwiseXor($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule312() {
         $this->semValue = new Expr\BinaryOp\Concat($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule313() {
         $this->semValue = new Expr\BinaryOp\Plus($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule314() {
         $this->semValue = new Expr\BinaryOp\Minus($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule315() {
         $this->semValue = new Expr\BinaryOp\Mul($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule316() {
         $this->semValue = new Expr\BinaryOp\Div($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule317() {
         $this->semValue = new Expr\BinaryOp\Mod($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule318() {
         $this->semValue = new Expr\BinaryOp\ShiftLeft($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule319() {
         $this->semValue = new Expr\BinaryOp\ShiftRight($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule320() {
         $this->semValue = new Expr\BinaryOp\Pow($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule321() {
         $this->semValue = new Expr\UnaryPlus($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule322() {
         $this->semValue = new Expr\UnaryMinus($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule323() {
         $this->semValue = new Expr\BooleanNot($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule324() {
         $this->semValue = new Expr\BitwiseNot($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule325() {
         $this->semValue = new Expr\BinaryOp\Identical($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule326() {
         $this->semValue = new Expr\BinaryOp\NotIdentical($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule327() {
         $this->semValue = new Expr\BinaryOp\Equal($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule328() {
         $this->semValue = new Expr\BinaryOp\NotEqual($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule329() {
         $this->semValue = new Expr\BinaryOp\Spaceship($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule330() {
         $this->semValue = new Expr\BinaryOp\Smaller($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule331() {
         $this->semValue = new Expr\BinaryOp\SmallerOrEqual($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule332() {
         $this->semValue = new Expr\BinaryOp\Greater($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule333() {
         $this->semValue = new Expr\BinaryOp\GreaterOrEqual($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule334() {
         $this->semValue = new Expr\Instanceof_($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule335() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule336() {
         $this->semValue = new Expr\Ternary($this->semStack[$this->stackPos-(5-1)], $this->semStack[$this->stackPos-(5-3)], $this->semStack[$this->stackPos-(5-5)], $this->startAttributeStack[$this->stackPos-(5-1)] + $this->endAttributes);
    }

    protected function reduceRule337() {
         $this->semValue = new Expr\Ternary($this->semStack[$this->stackPos-(4-1)], null, $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule338() {
         $this->semValue = new Expr\BinaryOp\Coalesce($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule339() {
         $this->semValue = new Expr\Isset_($this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule340() {
         $this->semValue = new Expr\Empty_($this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule341() {
         $this->semValue = new Expr\Include_($this->semStack[$this->stackPos-(2-2)], Expr\Include_::TYPE_INCLUDE, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule342() {
         $this->semValue = new Expr\Include_($this->semStack[$this->stackPos-(2-2)], Expr\Include_::TYPE_INCLUDE_ONCE, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule343() {
         $this->semValue = new Expr\Eval_($this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule344() {
         $this->semValue = new Expr\Include_($this->semStack[$this->stackPos-(2-2)], Expr\Include_::TYPE_REQUIRE, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule345() {
         $this->semValue = new Expr\Include_($this->semStack[$this->stackPos-(2-2)], Expr\Include_::TYPE_REQUIRE_ONCE, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule346() {
         $this->semValue = new Expr\Cast\Int_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule347() {
         $this->semValue = new Expr\Cast\Double($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule348() {
         $this->semValue = new Expr\Cast\String_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule349() {
         $this->semValue = new Expr\Cast\Array_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule350() {
         $this->semValue = new Expr\Cast\Object_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule351() {
         $this->semValue = new Expr\Cast\Bool_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule352() {
         $this->semValue = new Expr\Cast\Unset_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule353() {
         $attrs = $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes;
            $attrs['kind'] = strtolower($this->semStack[$this->stackPos-(2-1)]) === 'exit' ? Expr\Exit_::KIND_EXIT : Expr\Exit_::KIND_DIE;
            $this->semValue = new Expr\Exit_($this->semStack[$this->stackPos-(2-2)], $attrs);
    }

    protected function reduceRule354() {
         $this->semValue = new Expr\ErrorSuppress($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule355() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule356() {
         $this->semValue = new Expr\ShellExec($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule357() {
         $this->semValue = new Expr\Print_($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule358() {
         $this->semValue = new Expr\Yield_(null, null, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule359() {
         $this->semValue = new Expr\Yield_($this->semStack[$this->stackPos-(2-2)], null, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule360() {
         $this->semValue = new Expr\Yield_($this->semStack[$this->stackPos-(4-4)], $this->semStack[$this->stackPos-(4-2)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule361() {
         $this->semValue = new Expr\YieldFrom($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule362() {
         $this->semValue = new Expr\Closure(['static' => false, 'byRef' => $this->semStack[$this->stackPos-(10-2)], 'params' => $this->semStack[$this->stackPos-(10-4)], 'uses' => $this->semStack[$this->stackPos-(10-6)], 'returnType' => $this->semStack[$this->stackPos-(10-7)], 'stmts' => $this->semStack[$this->stackPos-(10-9)]], $this->startAttributeStack[$this->stackPos-(10-1)] + $this->endAttributes);
    }

    protected function reduceRule363() {
         $this->semValue = new Expr\Closure(['static' => true, 'byRef' => $this->semStack[$this->stackPos-(11-3)], 'params' => $this->semStack[$this->stackPos-(11-5)], 'uses' => $this->semStack[$this->stackPos-(11-7)], 'returnType' => $this->semStack[$this->stackPos-(11-8)], 'stmts' => $this->semStack[$this->stackPos-(11-10)]], $this->startAttributeStack[$this->stackPos-(11-1)] + $this->endAttributes);
    }

    protected function reduceRule364() {
         $this->semValue = array(new Stmt\Class_(null, ['type' => 0, 'extends' => $this->semStack[$this->stackPos-(7-3)], 'implements' => $this->semStack[$this->stackPos-(7-4)], 'stmts' => $this->semStack[$this->stackPos-(7-6)]], $this->startAttributeStack[$this->stackPos-(7-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(7-2)]);
    }

    protected function reduceRule365() {
         $this->semValue = new Expr\New_($this->semStack[$this->stackPos-(3-2)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule366() {
         list($class, $ctorArgs) = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = new Expr\New_($class, $ctorArgs, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule367() {
         $this->semValue = array();
    }

    protected function reduceRule368() {
         $this->semValue = $this->semStack[$this->stackPos-(4-3)];
    }

    protected function reduceRule369() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule370() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule371() {
         $this->semValue = new Expr\ClosureUse(substr($this->semStack[$this->stackPos-(2-2)], 1), $this->semStack[$this->stackPos-(2-1)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule372() {
         $this->semValue = new Expr\FuncCall($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule373() {
         $this->semValue = new Expr\FuncCall($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule374() {
         $this->semValue = new Expr\StaticCall($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule375() {
         $this->semValue = new Name($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule376() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule377() {
         $this->semValue = new Name($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule378() {
         $this->semValue = new Name\FullyQualified($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule379() {
         $this->semValue = new Name\Relative($this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule380() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule381() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule382() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule383() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule384() {
         $this->semValue = null;
    }

    protected function reduceRule385() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule386() {
         $this->semValue = array();
    }

    protected function reduceRule387() {
         $this->semValue = array(new Scalar\EncapsedStringPart(Scalar\String_::parseEscapeSequences($this->semStack[$this->stackPos-(1-1)], '`'), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes));
    }

    protected function reduceRule388() {
         foreach ($this->semStack[$this->stackPos-(1-1)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, '`', true); } }; $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule389() {
         $this->semValue = array();
    }

    protected function reduceRule390() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule391() {
         $this->semValue = new Expr\ConstFetch($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule392() {
         $this->semValue = new Expr\ClassConstFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule393() {
         $attrs = $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes; $attrs['kind'] = Expr\Array_::KIND_LONG;
            $this->semValue = new Expr\Array_($this->semStack[$this->stackPos-(4-3)], $attrs);
    }

    protected function reduceRule394() {
         $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = Expr\Array_::KIND_SHORT;
            $this->semValue = new Expr\Array_($this->semStack[$this->stackPos-(3-2)], $attrs);
    }

    protected function reduceRule395() {
         $attrs = $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes; $attrs['kind'] = ($this->semStack[$this->stackPos-(1-1)][0] === "'" || ($this->semStack[$this->stackPos-(1-1)][1] === "'" && ($this->semStack[$this->stackPos-(1-1)][0] === 'b' || $this->semStack[$this->stackPos-(1-1)][0] === 'B')) ? Scalar\String_::KIND_SINGLE_QUOTED : Scalar\String_::KIND_DOUBLE_QUOTED);
            $this->semValue = new Scalar\String_(Scalar\String_::parse($this->semStack[$this->stackPos-(1-1)]), $attrs);
    }

    protected function reduceRule396() {
         $this->semValue = Scalar\LNumber::fromString($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule397() {
         $this->semValue = new Scalar\DNumber(Scalar\DNumber::parse($this->semStack[$this->stackPos-(1-1)]), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule398() {
         $this->semValue = new Scalar\MagicConst\Line($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule399() {
         $this->semValue = new Scalar\MagicConst\File($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule400() {
         $this->semValue = new Scalar\MagicConst\Dir($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule401() {
         $this->semValue = new Scalar\MagicConst\Class_($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule402() {
         $this->semValue = new Scalar\MagicConst\Trait_($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule403() {
         $this->semValue = new Scalar\MagicConst\Method($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule404() {
         $this->semValue = new Scalar\MagicConst\Function_($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule405() {
         $this->semValue = new Scalar\MagicConst\Namespace_($this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule406() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule407() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule408() {
         $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];;
            $this->semValue = new Scalar\String_(Scalar\String_::parseDocString($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-2)]), $attrs);
    }

    protected function reduceRule409() {
         $attrs = $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(2-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(2-1)], $matches); $attrs['docLabel'] = $matches[1];;
            $this->semValue = new Scalar\String_('', $attrs);
    }

    protected function reduceRule410() {
         $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = Scalar\String_::KIND_DOUBLE_QUOTED;
            foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, '"', true); } }; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $attrs);
    }

    protected function reduceRule411() {
         $attrs = $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes; $attrs['kind'] = strpos($this->semStack[$this->stackPos-(3-1)], "'") === false ? Scalar\String_::KIND_HEREDOC : Scalar\String_::KIND_NOWDOC; preg_match('/\A[bB]?<<<[ \t]*[\'"]?([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)[\'"]?(?:\r\n|\n|\r)\z/', $this->semStack[$this->stackPos-(3-1)], $matches); $attrs['docLabel'] = $matches[1];;
            foreach ($this->semStack[$this->stackPos-(3-2)] as $s) { if ($s instanceof Node\Scalar\EncapsedStringPart) { $s->value = Node\Scalar\String_::parseEscapeSequences($s->value, null, true); } } $s->value = preg_replace('~(\r\n|\n|\r)\z~', '', $s->value); if ('' === $s->value) array_pop($this->semStack[$this->stackPos-(3-2)]);; $this->semValue = new Scalar\Encapsed($this->semStack[$this->stackPos-(3-2)], $attrs);
    }

    protected function reduceRule412() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule413() {
        $this->semValue = $this->semStack[$this->stackPos];
    }

    protected function reduceRule414() {
         $this->semValue = null;
    }

    protected function reduceRule415() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule416() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule417() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule418() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule419() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule420() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule421() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule422() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule423() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule424() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule425() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule426() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule427() {
         $this->semValue = new Expr\MethodCall($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->semStack[$this->stackPos-(4-4)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule428() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule429() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule430() {
         $this->semValue = new Expr\PropertyFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule431() {
         $this->semValue = substr($this->semStack[$this->stackPos-(1-1)], 1);
    }

    protected function reduceRule432() {
         $this->semValue = $this->semStack[$this->stackPos-(4-3)];
    }

    protected function reduceRule433() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(2-2)], $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule434() {
         $this->semValue = new Expr\StaticPropertyFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule435() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule436() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule437() {
         $this->semValue = new Expr\ArrayDimFetch($this->semStack[$this->stackPos-(4-1)], $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule438() {
         $this->semValue = new Expr\PropertyFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule439() {
         $this->semValue = new Expr\StaticPropertyFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule440() {
         $this->semValue = new Expr\StaticPropertyFetch($this->semStack[$this->stackPos-(3-1)], $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule441() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule442() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule443() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule444() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule445() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule446() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule447() {
         $this->semValue = new Expr\List_($this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule448() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule449() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule450() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule451() {
         $this->semValue = $this->semStack[$this->stackPos-(1-1)];
    }

    protected function reduceRule452() {
         $this->semValue = null;
    }

    protected function reduceRule453() {
         $this->semValue = array();
    }

    protected function reduceRule454() {
         $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule455() {
         $this->semStack[$this->stackPos-(3-1)][] = $this->semStack[$this->stackPos-(3-3)]; $this->semValue = $this->semStack[$this->stackPos-(3-1)];
    }

    protected function reduceRule456() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule457() {
         $this->semValue = new Expr\ArrayItem($this->semStack[$this->stackPos-(3-3)], $this->semStack[$this->stackPos-(3-1)], false, $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule458() {
         $this->semValue = new Expr\ArrayItem($this->semStack[$this->stackPos-(1-1)], null, false, $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule459() {
         $this->semValue = new Expr\ArrayItem($this->semStack[$this->stackPos-(4-4)], $this->semStack[$this->stackPos-(4-1)], true, $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule460() {
         $this->semValue = new Expr\ArrayItem($this->semStack[$this->stackPos-(2-2)], null, true, $this->startAttributeStack[$this->stackPos-(2-1)] + $this->endAttributes);
    }

    protected function reduceRule461() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule462() {
         $this->semStack[$this->stackPos-(2-1)][] = $this->semStack[$this->stackPos-(2-2)]; $this->semValue = $this->semStack[$this->stackPos-(2-1)];
    }

    protected function reduceRule463() {
         $this->semValue = array($this->semStack[$this->stackPos-(1-1)]);
    }

    protected function reduceRule464() {
         $this->semValue = array($this->semStack[$this->stackPos-(2-1)], $this->semStack[$this->stackPos-(2-2)]);
    }

    protected function reduceRule465() {
         $this->semValue = new Scalar\EncapsedStringPart($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule466() {
         $this->semValue = new Expr\Variable(substr($this->semStack[$this->stackPos-(1-1)], 1), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule467() {
         $this->semValue = new Expr\ArrayDimFetch(new Expr\Variable(substr($this->semStack[$this->stackPos-(4-1)], 1), $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(4-3)], $this->startAttributeStack[$this->stackPos-(4-1)] + $this->endAttributes);
    }

    protected function reduceRule468() {
         $this->semValue = new Expr\PropertyFetch(new Expr\Variable(substr($this->semStack[$this->stackPos-(3-1)], 1), $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(3-3)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule469() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule470() {
         $this->semValue = new Expr\Variable($this->semStack[$this->stackPos-(3-2)], $this->startAttributeStack[$this->stackPos-(3-1)] + $this->endAttributes);
    }

    protected function reduceRule471() {
         $this->semValue = new Expr\ArrayDimFetch(new Expr\Variable($this->semStack[$this->stackPos-(6-2)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes), $this->semStack[$this->stackPos-(6-4)], $this->startAttributeStack[$this->stackPos-(6-1)] + $this->endAttributes);
    }

    protected function reduceRule472() {
         $this->semValue = $this->semStack[$this->stackPos-(3-2)];
    }

    protected function reduceRule473() {
         $this->semValue = new Scalar\String_($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule474() {
         $this->semValue = new Scalar\String_($this->semStack[$this->stackPos-(1-1)], $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }

    protected function reduceRule475() {
         $this->semValue = new Expr\Variable(substr($this->semStack[$this->stackPos-(1-1)], 1), $this->startAttributeStack[$this->stackPos-(1-1)] + $this->endAttributes);
    }
}
lexer = $lexer;
        $this->errors = array();
        $this->throwOnError = isset($options['throwOnError']) ? $options['throwOnError'] : true;
    }

    /**
     * Get array of errors that occurred during the last parse.
     *
     * This method may only return multiple errors if the 'throwOnError' option is disabled.
     *
     * @return Error[]
     */
    public function getErrors() {
        return $this->errors;
    }

    /**
     * Parses PHP code into a node tree.
     *
     * @param string $code The source code to parse
     *
     * @return Node[]|null Array of statements (or null if the 'throwOnError' option is disabled and the parser was
     *                     unable to recover from an error).
     */
    public function parse($code) {
        $this->lexer->startLexing($code);
        $this->errors = array();

        // We start off with no lookahead-token
        $symbol = self::SYMBOL_NONE;

        // The attributes for a node are taken from the first and last token of the node.
        // From the first token only the startAttributes are taken and from the last only
        // the endAttributes. Both are merged using the array union operator (+).
        $startAttributes = '*POISON';
        $endAttributes = '*POISON';
        $this->endAttributes = $endAttributes;

        // In order to figure out the attributes for the starting token, we have to keep
        // them in a stack
        $this->startAttributeStack = array();

        // Start off in the initial state and keep a stack of previous states
        $state = 0;
        $stateStack = array($state);

        // Semantic value stack (contains values of tokens and semantic action results)
        $this->semStack = array();

        // Current position in the stack(s)
        $this->stackPos = 0;

        $errorState = 0;

        for (;;) {
            //$this->traceNewState($state, $symbol);

            if ($this->actionBase[$state] == 0) {
                $rule = $this->actionDefault[$state];
            } else {
                if ($symbol === self::SYMBOL_NONE) {
                    // Fetch the next token id from the lexer and fetch additional info by-ref.
                    // The end attributes are fetched into a temporary variable and only set once the token is really
                    // shifted (not during read). Otherwise you would sometimes get off-by-one errors, when a rule is
                    // reduced after a token was read but not yet shifted.
                    $tokenId = $this->lexer->getNextToken($tokenValue, $startAttributes, $endAttributes);

                    // map the lexer token id to the internally used symbols
                    $symbol = $tokenId >= 0 && $tokenId < $this->tokenToSymbolMapSize
                        ? $this->tokenToSymbol[$tokenId]
                        : $this->invalidSymbol;

                    if ($symbol === $this->invalidSymbol) {
                        throw new \RangeException(sprintf(
                            'The lexer returned an invalid token (id=%d, value=%s)',
                            $tokenId, $tokenValue
                        ));
                    }

                    // This is necessary to assign some meaningful attributes to /* empty */ productions. They'll get
                    // the attributes of the next token, even though they don't contain it themselves.
                    $this->startAttributeStack[$this->stackPos+1] = $startAttributes;
                    $this->lookaheadStartAttributes = $startAttributes;

                    //$this->traceRead($symbol);
                }

                $idx = $this->actionBase[$state] + $symbol;
                if ((($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] == $symbol)
                     || ($state < $this->YY2TBLSTATE
                         && ($idx = $this->actionBase[$state + $this->YYNLSTATES] + $symbol) >= 0
                         && $idx < $this->actionTableSize && $this->actionCheck[$idx] == $symbol))
                    && ($action = $this->action[$idx]) != $this->defaultAction) {
                    /*
                     * >= YYNLSTATES: shift and reduce
                     * > 0: shift
                     * = 0: accept
                     * < 0: reduce
                     * = -YYUNEXPECTED: error
                     */
                    if ($action > 0) {
                        /* shift */
                        //$this->traceShift($symbol);

                        ++$this->stackPos;
                        $stateStack[$this->stackPos] = $state = $action;
                        $this->semStack[$this->stackPos] = $tokenValue;
                        $this->startAttributeStack[$this->stackPos] = $startAttributes;
                        $this->endAttributes = $endAttributes;
                        $symbol = self::SYMBOL_NONE;

                        if ($errorState) {
                            --$errorState;
                        }

                        if ($action < $this->YYNLSTATES) {
                            continue;
                        }

                        /* $yyn >= YYNLSTATES means shift-and-reduce */
                        $rule = $action - $this->YYNLSTATES;
                    } else {
                        $rule = -$action;
                    }
                } else {
                    $rule = $this->actionDefault[$state];
                }
            }

            for (;;) {
                if ($rule === 0) {
                    /* accept */
                    //$this->traceAccept();
                    return $this->semValue;
                } elseif ($rule !== $this->unexpectedTokenRule) {
                    /* reduce */
                    //$this->traceReduce($rule);

                    try {
                        $this->{'reduceRule' . $rule}();
                    } catch (Error $e) {
                        if (-1 === $e->getStartLine() && isset($startAttributes['startLine'])) {
                            $e->setStartLine($startAttributes['startLine']);
                        }

                        $this->errors[] = $e;
                        if ($this->throwOnError) {
                            throw $e;
                        } else {
                            // Currently can't recover from "special" errors
                            return null;
                        }
                    }

                    /* Goto - shift nonterminal */
                    $this->stackPos -= $this->ruleToLength[$rule];
                    $nonTerminal = $this->ruleToNonTerminal[$rule];
                    $idx = $this->gotoBase[$nonTerminal] + $stateStack[$this->stackPos];
                    if ($idx >= 0 && $idx < $this->gotoTableSize && $this->gotoCheck[$idx] == $nonTerminal) {
                        $state = $this->goto[$idx];
                    } else {
                        $state = $this->gotoDefault[$nonTerminal];
                    }

                    ++$this->stackPos;
                    $stateStack[$this->stackPos]     = $state;
                    $this->semStack[$this->stackPos] = $this->semValue;
                } else {
                    /* error */
                    switch ($errorState) {
                        case 0:
                            $msg = $this->getErrorMessage($symbol, $state);
                            $error = new Error($msg, $startAttributes + $endAttributes);
                            $this->errors[] = $error;
                            if ($this->throwOnError) {
                                throw $error;
                            }
                            // Break missing intentionally
                        case 1:
                        case 2:
                            $errorState = 3;

                            // Pop until error-expecting state uncovered
                            while (!(
                                (($idx = $this->actionBase[$state] + $this->errorSymbol) >= 0
                                    && $idx < $this->actionTableSize && $this->actionCheck[$idx] == $this->errorSymbol)
                                || ($state < $this->YY2TBLSTATE
                                    && ($idx = $this->actionBase[$state + $this->YYNLSTATES] + $this->errorSymbol) >= 0
                                    && $idx < $this->actionTableSize && $this->actionCheck[$idx] == $this->errorSymbol)
                            ) || ($action = $this->action[$idx]) == $this->defaultAction) { // Not totally sure about this
                                if ($this->stackPos <= 0) {
                                    // Could not recover from error
                                    return null;
                                }
                                $state = $stateStack[--$this->stackPos];
                                //$this->tracePop($state);
                            }

                            //$this->traceShift($this->errorSymbol);
                            $stateStack[++$this->stackPos] = $state = $action;
                            break;

                        case 3:
                            if ($symbol === 0) {
                                // Reached EOF without recovering from error
                                return null;
                            }

                            //$this->traceDiscard($symbol);
                            $symbol = self::SYMBOL_NONE;
                            break 2;
                    }
                }

                if ($state < $this->YYNLSTATES) {
                    break;
                }

                /* >= YYNLSTATES means shift-and-reduce */
                $rule = $state - $this->YYNLSTATES;
            }
        }

        throw new \RuntimeException('Reached end of parser loop');
    }

    protected function getErrorMessage($symbol, $state) {
        $expectedString = '';
        if ($expected = $this->getExpectedTokens($state)) {
            $expectedString = ', expecting ' . implode(' or ', $expected);
        }

        return 'Syntax error, unexpected ' . $this->symbolToName[$symbol] . $expectedString;
    }

    protected function getExpectedTokens($state) {
        $expected = array();

        $base = $this->actionBase[$state];
        foreach ($this->symbolToName as $symbol => $name) {
            $idx = $base + $symbol;
            if ($idx >= 0 && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol
                || $state < $this->YY2TBLSTATE
                && ($idx = $this->actionBase[$state + $this->YYNLSTATES] + $symbol) >= 0
                && $idx < $this->actionTableSize && $this->actionCheck[$idx] === $symbol
            ) {
                if ($this->action[$idx] != $this->unexpectedTokenRule
                    && $this->action[$idx] != $this->defaultAction
                ) {
                    if (count($expected) == 4) {
                        /* Too many expected tokens */
                        return array();
                    }

                    $expected[] = $name;
                }
            }
        }

        return $expected;
    }

    /*
     * Tracing functions used for debugging the parser.
     */

    /*
    protected function traceNewState($state, $symbol) {
        echo '% State ' . $state
            . ', Lookahead ' . ($symbol == self::SYMBOL_NONE ? '--none--' : $this->symbolToName[$symbol]) . "\n";
    }

    protected function traceRead($symbol) {
        echo '% Reading ' . $this->symbolToName[$symbol] . "\n";
    }

    protected function traceShift($symbol) {
        echo '% Shift ' . $this->symbolToName[$symbol] . "\n";
    }

    protected function traceAccept() {
        echo "% Accepted.\n";
    }

    protected function traceReduce($n) {
        echo '% Reduce by (' . $n . ') ' . $this->productions[$n] . "\n";
    }

    protected function tracePop($state) {
        echo '% Recovering, uncovered state ' . $state . "\n";
    }

    protected function traceDiscard($symbol) {
        echo '% Discard ' . $this->symbolToName[$symbol] . "\n";
    }
    */

    /*
     * Helper functions invoked by semantic actions
     */

    /**
     * Moves statements of semicolon-style namespaces into $ns->stmts and checks various error conditions.
     *
     * @param Node[] $stmts
     * @return Node[]
     */
    protected function handleNamespaces(array $stmts) {
        $style = $this->getNamespacingStyle($stmts);
        if (null === $style) {
            // not namespaced, nothing to do
            return $stmts;
        } elseif ('brace' === $style) {
            // For braced namespaces we only have to check that there are no invalid statements between the namespaces
            $afterFirstNamespace = false;
            foreach ($stmts as $stmt) {
                if ($stmt instanceof Node\Stmt\Namespace_) {
                    $afterFirstNamespace = true;
                } elseif (!$stmt instanceof Node\Stmt\HaltCompiler && $afterFirstNamespace) {
                    throw new Error('No code may exist outside of namespace {}', $stmt->getLine());
                }
            }
            return $stmts;
        } else {
            // For semicolon namespaces we have to move the statements after a namespace declaration into ->stmts
            $resultStmts = array();
            $targetStmts =& $resultStmts;
            foreach ($stmts as $stmt) {
                if ($stmt instanceof Node\Stmt\Namespace_) {
                    $stmt->stmts = array();
                    $targetStmts =& $stmt->stmts;
                    $resultStmts[] = $stmt;
                } elseif ($stmt instanceof Node\Stmt\HaltCompiler) {
                    // __halt_compiler() is not moved into the namespace
                    $resultStmts[] = $stmt;
                } else {
                    $targetStmts[] = $stmt;
                }
            }
            return $resultStmts;
        }
    }

    private function getNamespacingStyle(array $stmts) {
        $style = null;
        $hasNotAllowedStmts = false;
        foreach ($stmts as $i => $stmt) {
            if ($stmt instanceof Node\Stmt\Namespace_) {
                $currentStyle = null === $stmt->stmts ? 'semicolon' : 'brace';
                if (null === $style) {
                    $style = $currentStyle;
                    if ($hasNotAllowedStmts) {
                        throw new Error('Namespace declaration statement has to be the very first statement in the script', $stmt->getLine());
                    }
                } elseif ($style !== $currentStyle) {
                    throw new Error('Cannot mix bracketed namespace declarations with unbracketed namespace declarations', $stmt->getLine());
                }
                continue;
            }

            /* declare(), __halt_compiler() and nops can be used before a namespace declaration */
            if ($stmt instanceof Node\Stmt\Declare_
                || $stmt instanceof Node\Stmt\HaltCompiler
                || $stmt instanceof Node\Stmt\Nop) {
                continue;
            }

            /* There may be a hashbang line at the very start of the file */
            if ($i == 0 && $stmt instanceof Node\Stmt\InlineHTML && preg_match('/\A#!.*\r?\n\z/', $stmt->value)) {
                continue;
            }

            /* Everything else if forbidden before namespace declarations */
            $hasNotAllowedStmts = true;
        }
        return $style;
    }

    protected function handleScalarTypes(Name $name) {
        $scalarTypes = [
            'bool'   => true,
            'int'    => true,
            'float'  => true,
            'string' => true,
        ];

        if (!$name->isUnqualified()) {
            return $name;
        }

        $lowerName = strtolower($name->toString());
        return isset($scalarTypes[$lowerName]) ? $lowerName : $name;
    }
}
type ? $this->pType($node->type) . ' ' : '')
             . ($node->byRef ? '&' : '')
             . ($node->variadic ? '...' : '')
             . '$' . $node->name
             . ($node->default ? ' = ' . $this->p($node->default) : '');
    }

    public function pArg(Node\Arg $node) {
        return ($node->byRef ? '&' : '') . ($node->unpack ? '...' : '') . $this->p($node->value);
    }

    public function pConst(Node\Const_ $node) {
        return $node->name . ' = ' . $this->p($node->value);
    }

    // Names

    public function pName(Name $node) {
        return implode('\\', $node->parts);
    }

    public function pName_FullyQualified(Name\FullyQualified $node) {
        return '\\' . implode('\\', $node->parts);
    }

    public function pName_Relative(Name\Relative $node) {
        return 'namespace\\' . implode('\\', $node->parts);
    }

    // Magic Constants

    public function pScalar_MagicConst_Class(MagicConst\Class_ $node) {
        return '__CLASS__';
    }

    public function pScalar_MagicConst_Dir(MagicConst\Dir $node) {
        return '__DIR__';
    }

    public function pScalar_MagicConst_File(MagicConst\File $node) {
        return '__FILE__';
    }

    public function pScalar_MagicConst_Function(MagicConst\Function_ $node) {
        return '__FUNCTION__';
    }

    public function pScalar_MagicConst_Line(MagicConst\Line $node) {
        return '__LINE__';
    }

    public function pScalar_MagicConst_Method(MagicConst\Method $node) {
        return '__METHOD__';
    }

    public function pScalar_MagicConst_Namespace(MagicConst\Namespace_ $node) {
        return '__NAMESPACE__';
    }

    public function pScalar_MagicConst_Trait(MagicConst\Trait_ $node) {
        return '__TRAIT__';
    }

    // Scalars

    public function pScalar_String(Scalar\String_ $node) {
        $kind = $node->getAttribute('kind', Scalar\String_::KIND_SINGLE_QUOTED);
        switch ($kind) {
            case Scalar\String_::KIND_NOWDOC:
                $label = $node->getAttribute('docLabel');
                if ($label && !$this->containsEndLabel($node->value, $label)) {
                    if ($node->value === '') {
                        return $this->pNoIndent("<<<'$label'\n$label") . $this->docStringEndToken;
                    }

                    return $this->pNoIndent("<<<'$label'\n$node->value\n$label")
                         . $this->docStringEndToken;
                }
                /* break missing intentionally */
            case Scalar\String_::KIND_SINGLE_QUOTED:
                return '\'' . $this->pNoIndent(addcslashes($node->value, '\'\\')) . '\'';
            case Scalar\String_::KIND_HEREDOC:
                $label = $node->getAttribute('docLabel');
                if ($label && !$this->containsEndLabel($node->value, $label)) {
                    if ($node->value === '') {
                        return $this->pNoIndent("<<<$label\n$label") . $this->docStringEndToken;
                    }

                    $escaped = $this->escapeString($node->value, null);
                    return $this->pNoIndent("<<<$label\n" . $escaped ."\n$label")
                         . $this->docStringEndToken;
                }
            /* break missing intentionally */
            case Scalar\String_::KIND_DOUBLE_QUOTED:
                return '"' . $this->escapeString($node->value, '"') . '"';
        }
        throw new \Exception('Invalid string kind');
    }

    public function pScalar_Encapsed(Scalar\Encapsed $node) {
        if ($node->getAttribute('kind') === Scalar\String_::KIND_HEREDOC) {
            $label = $node->getAttribute('docLabel');
            if ($label && !$this->encapsedContainsEndLabel($node->parts, $label)) {
                if (count($node->parts) === 1
                    && $node->parts[0] instanceof Scalar\EncapsedStringPart
                    && $node->parts[0]->value === ''
                ) {
                    return $this->pNoIndent("<<<$label\n$label") . $this->docStringEndToken;
                }

                return $this->pNoIndent(
                    "<<<$label\n" . $this->pEncapsList($node->parts, null) . "\n$label"
                ) . $this->docStringEndToken;
            }
        }
        return '"' . $this->pEncapsList($node->parts, '"') . '"';
    }

    public function pScalar_LNumber(Scalar\LNumber $node) {
        $str = (string) $node->value;
        switch ($node->getAttribute('kind', Scalar\LNumber::KIND_DEC)) {
            case Scalar\LNumber::KIND_BIN:
                return '0b' . base_convert($str, 10, 2);
            case Scalar\LNumber::KIND_OCT:
                return '0' . base_convert($str, 10, 8);
            case Scalar\LNumber::KIND_DEC:
                return $str;
            case Scalar\LNumber::KIND_HEX:
                return '0x' . base_convert($str, 10, 16);
        }
        throw new \Exception('Invalid number kind');
    }

    public function pScalar_DNumber(Scalar\DNumber $node) {
        $stringValue = sprintf('%.16G', $node->value);
        if ($node->value !== (double) $stringValue) {
            $stringValue = sprintf('%.17G', $node->value);
        }

        // ensure that number is really printed as float
        return preg_match('/^-?[0-9]+$/', $stringValue) ? $stringValue . '.0' : $stringValue;
    }

    // Assignments

    public function pExpr_Assign(Expr\Assign $node) {
        return $this->pInfixOp('Expr_Assign', $node->var, ' = ', $node->expr);
    }

    public function pExpr_AssignRef(Expr\AssignRef $node) {
        return $this->pInfixOp('Expr_AssignRef', $node->var, ' =& ', $node->expr);
    }

    public function pExpr_AssignOp_Plus(AssignOp\Plus $node) {
        return $this->pInfixOp('Expr_AssignOp_Plus', $node->var, ' += ', $node->expr);
    }

    public function pExpr_AssignOp_Minus(AssignOp\Minus $node) {
        return $this->pInfixOp('Expr_AssignOp_Minus', $node->var, ' -= ', $node->expr);
    }

    public function pExpr_AssignOp_Mul(AssignOp\Mul $node) {
        return $this->pInfixOp('Expr_AssignOp_Mul', $node->var, ' *= ', $node->expr);
    }

    public function pExpr_AssignOp_Div(AssignOp\Div $node) {
        return $this->pInfixOp('Expr_AssignOp_Div', $node->var, ' /= ', $node->expr);
    }

    public function pExpr_AssignOp_Concat(AssignOp\Concat $node) {
        return $this->pInfixOp('Expr_AssignOp_Concat', $node->var, ' .= ', $node->expr);
    }

    public function pExpr_AssignOp_Mod(AssignOp\Mod $node) {
        return $this->pInfixOp('Expr_AssignOp_Mod', $node->var, ' %= ', $node->expr);
    }

    public function pExpr_AssignOp_BitwiseAnd(AssignOp\BitwiseAnd $node) {
        return $this->pInfixOp('Expr_AssignOp_BitwiseAnd', $node->var, ' &= ', $node->expr);
    }

    public function pExpr_AssignOp_BitwiseOr(AssignOp\BitwiseOr $node) {
        return $this->pInfixOp('Expr_AssignOp_BitwiseOr', $node->var, ' |= ', $node->expr);
    }

    public function pExpr_AssignOp_BitwiseXor(AssignOp\BitwiseXor $node) {
        return $this->pInfixOp('Expr_AssignOp_BitwiseXor', $node->var, ' ^= ', $node->expr);
    }

    public function pExpr_AssignOp_ShiftLeft(AssignOp\ShiftLeft $node) {
        return $this->pInfixOp('Expr_AssignOp_ShiftLeft', $node->var, ' <<= ', $node->expr);
    }

    public function pExpr_AssignOp_ShiftRight(AssignOp\ShiftRight $node) {
        return $this->pInfixOp('Expr_AssignOp_ShiftRight', $node->var, ' >>= ', $node->expr);
    }

    public function pExpr_AssignOp_Pow(AssignOp\Pow $node) {
        return $this->pInfixOp('Expr_AssignOp_Pow', $node->var, ' **= ', $node->expr);
    }

    // Binary expressions

    public function pExpr_BinaryOp_Plus(BinaryOp\Plus $node) {
        return $this->pInfixOp('Expr_BinaryOp_Plus', $node->left, ' + ', $node->right);
    }

    public function pExpr_BinaryOp_Minus(BinaryOp\Minus $node) {
        return $this->pInfixOp('Expr_BinaryOp_Minus', $node->left, ' - ', $node->right);
    }

    public function pExpr_BinaryOp_Mul(BinaryOp\Mul $node) {
        return $this->pInfixOp('Expr_BinaryOp_Mul', $node->left, ' * ', $node->right);
    }

    public function pExpr_BinaryOp_Div(BinaryOp\Div $node) {
        return $this->pInfixOp('Expr_BinaryOp_Div', $node->left, ' / ', $node->right);
    }

    public function pExpr_BinaryOp_Concat(BinaryOp\Concat $node) {
        return $this->pInfixOp('Expr_BinaryOp_Concat', $node->left, ' . ', $node->right);
    }

    public function pExpr_BinaryOp_Mod(BinaryOp\Mod $node) {
        return $this->pInfixOp('Expr_BinaryOp_Mod', $node->left, ' % ', $node->right);
    }

    public function pExpr_BinaryOp_BooleanAnd(BinaryOp\BooleanAnd $node) {
        return $this->pInfixOp('Expr_BinaryOp_BooleanAnd', $node->left, ' && ', $node->right);
    }

    public function pExpr_BinaryOp_BooleanOr(BinaryOp\BooleanOr $node) {
        return $this->pInfixOp('Expr_BinaryOp_BooleanOr', $node->left, ' || ', $node->right);
    }

    public function pExpr_BinaryOp_BitwiseAnd(BinaryOp\BitwiseAnd $node) {
        return $this->pInfixOp('Expr_BinaryOp_BitwiseAnd', $node->left, ' & ', $node->right);
    }

    public function pExpr_BinaryOp_BitwiseOr(BinaryOp\BitwiseOr $node) {
        return $this->pInfixOp('Expr_BinaryOp_BitwiseOr', $node->left, ' | ', $node->right);
    }

    public function pExpr_BinaryOp_BitwiseXor(BinaryOp\BitwiseXor $node) {
        return $this->pInfixOp('Expr_BinaryOp_BitwiseXor', $node->left, ' ^ ', $node->right);
    }

    public function pExpr_BinaryOp_ShiftLeft(BinaryOp\ShiftLeft $node) {
        return $this->pInfixOp('Expr_BinaryOp_ShiftLeft', $node->left, ' << ', $node->right);
    }

    public function pExpr_BinaryOp_ShiftRight(BinaryOp\ShiftRight $node) {
        return $this->pInfixOp('Expr_BinaryOp_ShiftRight', $node->left, ' >> ', $node->right);
    }

    public function pExpr_BinaryOp_Pow(BinaryOp\Pow $node) {
        return $this->pInfixOp('Expr_BinaryOp_Pow', $node->left, ' ** ', $node->right);
    }

    public function pExpr_BinaryOp_LogicalAnd(BinaryOp\LogicalAnd $node) {
        return $this->pInfixOp('Expr_BinaryOp_LogicalAnd', $node->left, ' and ', $node->right);
    }

    public function pExpr_BinaryOp_LogicalOr(BinaryOp\LogicalOr $node) {
        return $this->pInfixOp('Expr_BinaryOp_LogicalOr', $node->left, ' or ', $node->right);
    }

    public function pExpr_BinaryOp_LogicalXor(BinaryOp\LogicalXor $node) {
        return $this->pInfixOp('Expr_BinaryOp_LogicalXor', $node->left, ' xor ', $node->right);
    }

    public function pExpr_BinaryOp_Equal(BinaryOp\Equal $node) {
        return $this->pInfixOp('Expr_BinaryOp_Equal', $node->left, ' == ', $node->right);
    }

    public function pExpr_BinaryOp_NotEqual(BinaryOp\NotEqual $node) {
        return $this->pInfixOp('Expr_BinaryOp_NotEqual', $node->left, ' != ', $node->right);
    }

    public function pExpr_BinaryOp_Identical(BinaryOp\Identical $node) {
        return $this->pInfixOp('Expr_BinaryOp_Identical', $node->left, ' === ', $node->right);
    }

    public function pExpr_BinaryOp_NotIdentical(BinaryOp\NotIdentical $node) {
        return $this->pInfixOp('Expr_BinaryOp_NotIdentical', $node->left, ' !== ', $node->right);
    }

    public function pExpr_BinaryOp_Spaceship(BinaryOp\Spaceship $node) {
        return $this->pInfixOp('Expr_BinaryOp_Spaceship', $node->left, ' <=> ', $node->right);
    }

    public function pExpr_BinaryOp_Greater(BinaryOp\Greater $node) {
        return $this->pInfixOp('Expr_BinaryOp_Greater', $node->left, ' > ', $node->right);
    }

    public function pExpr_BinaryOp_GreaterOrEqual(BinaryOp\GreaterOrEqual $node) {
        return $this->pInfixOp('Expr_BinaryOp_GreaterOrEqual', $node->left, ' >= ', $node->right);
    }

    public function pExpr_BinaryOp_Smaller(BinaryOp\Smaller $node) {
        return $this->pInfixOp('Expr_BinaryOp_Smaller', $node->left, ' < ', $node->right);
    }

    public function pExpr_BinaryOp_SmallerOrEqual(BinaryOp\SmallerOrEqual $node) {
        return $this->pInfixOp('Expr_BinaryOp_SmallerOrEqual', $node->left, ' <= ', $node->right);
    }

    public function pExpr_BinaryOp_Coalesce(BinaryOp\Coalesce $node) {
        return $this->pInfixOp('Expr_BinaryOp_Coalesce', $node->left, ' ?? ', $node->right);
    }

    public function pExpr_Instanceof(Expr\Instanceof_ $node) {
        return $this->pInfixOp('Expr_Instanceof', $node->expr, ' instanceof ', $node->class);
    }

    // Unary expressions

    public function pExpr_BooleanNot(Expr\BooleanNot $node) {
        return $this->pPrefixOp('Expr_BooleanNot', '!', $node->expr);
    }

    public function pExpr_BitwiseNot(Expr\BitwiseNot $node) {
        return $this->pPrefixOp('Expr_BitwiseNot', '~', $node->expr);
    }

    public function pExpr_UnaryMinus(Expr\UnaryMinus $node) {
        return $this->pPrefixOp('Expr_UnaryMinus', '-', $node->expr);
    }

    public function pExpr_UnaryPlus(Expr\UnaryPlus $node) {
        return $this->pPrefixOp('Expr_UnaryPlus', '+', $node->expr);
    }

    public function pExpr_PreInc(Expr\PreInc $node) {
        return $this->pPrefixOp('Expr_PreInc', '++', $node->var);
    }

    public function pExpr_PreDec(Expr\PreDec $node) {
        return $this->pPrefixOp('Expr_PreDec', '--', $node->var);
    }

    public function pExpr_PostInc(Expr\PostInc $node) {
        return $this->pPostfixOp('Expr_PostInc', $node->var, '++');
    }

    public function pExpr_PostDec(Expr\PostDec $node) {
        return $this->pPostfixOp('Expr_PostDec', $node->var, '--');
    }

    public function pExpr_ErrorSuppress(Expr\ErrorSuppress $node) {
        return $this->pPrefixOp('Expr_ErrorSuppress', '@', $node->expr);
    }

    public function pExpr_YieldFrom(Expr\YieldFrom $node) {
        return $this->pPrefixOp('Expr_YieldFrom', 'yield from ', $node->expr);
    }

    public function pExpr_Print(Expr\Print_ $node) {
        return $this->pPrefixOp('Expr_Print', 'print ', $node->expr);
    }

    // Casts

    public function pExpr_Cast_Int(Cast\Int_ $node) {
        return $this->pPrefixOp('Expr_Cast_Int', '(int) ', $node->expr);
    }

    public function pExpr_Cast_Double(Cast\Double $node) {
        return $this->pPrefixOp('Expr_Cast_Double', '(double) ', $node->expr);
    }

    public function pExpr_Cast_String(Cast\String_ $node) {
        return $this->pPrefixOp('Expr_Cast_String', '(string) ', $node->expr);
    }

    public function pExpr_Cast_Array(Cast\Array_ $node) {
        return $this->pPrefixOp('Expr_Cast_Array', '(array) ', $node->expr);
    }

    public function pExpr_Cast_Object(Cast\Object_ $node) {
        return $this->pPrefixOp('Expr_Cast_Object', '(object) ', $node->expr);
    }

    public function pExpr_Cast_Bool(Cast\Bool_ $node) {
        return $this->pPrefixOp('Expr_Cast_Bool', '(bool) ', $node->expr);
    }

    public function pExpr_Cast_Unset(Cast\Unset_ $node) {
        return $this->pPrefixOp('Expr_Cast_Unset', '(unset) ', $node->expr);
    }

    // Function calls and similar constructs

    public function pExpr_FuncCall(Expr\FuncCall $node) {
        return $this->pCallLhs($node->name)
             . '(' . $this->pCommaSeparated($node->args) . ')';
    }

    public function pExpr_MethodCall(Expr\MethodCall $node) {
        return $this->pDereferenceLhs($node->var) . '->' . $this->pObjectProperty($node->name)
             . '(' . $this->pCommaSeparated($node->args) . ')';
    }

    public function pExpr_StaticCall(Expr\StaticCall $node) {
        return $this->pDereferenceLhs($node->class) . '::'
             . ($node->name instanceof Expr
                ? ($node->name instanceof Expr\Variable
                   ? $this->p($node->name)
                   : '{' . $this->p($node->name) . '}')
                : $node->name)
             . '(' . $this->pCommaSeparated($node->args) . ')';
    }

    public function pExpr_Empty(Expr\Empty_ $node) {
        return 'empty(' . $this->p($node->expr) . ')';
    }

    public function pExpr_Isset(Expr\Isset_ $node) {
        return 'isset(' . $this->pCommaSeparated($node->vars) . ')';
    }

    public function pExpr_Eval(Expr\Eval_ $node) {
        return 'eval(' . $this->p($node->expr) . ')';
    }

    public function pExpr_Include(Expr\Include_ $node) {
        static $map = array(
            Expr\Include_::TYPE_INCLUDE      => 'include',
            Expr\Include_::TYPE_INCLUDE_ONCE => 'include_once',
            Expr\Include_::TYPE_REQUIRE      => 'require',
            Expr\Include_::TYPE_REQUIRE_ONCE => 'require_once',
        );

        return $map[$node->type] . ' ' . $this->p($node->expr);
    }

    public function pExpr_List(Expr\List_ $node) {
        $pList = array();
        foreach ($node->vars as $var) {
            if (null === $var) {
                $pList[] = '';
            } else {
                $pList[] = $this->p($var);
            }
        }

        return 'list(' . implode(', ', $pList) . ')';
    }

    // Other

    public function pExpr_Variable(Expr\Variable $node) {
        if ($node->name instanceof Expr) {
            return '${' . $this->p($node->name) . '}';
        } else {
            return '$' . $node->name;
        }
    }

    public function pExpr_Array(Expr\Array_ $node) {
        $syntax = $node->getAttribute('kind',
            $this->options['shortArraySyntax'] ? Expr\Array_::KIND_SHORT : Expr\Array_::KIND_LONG);
        if ($syntax === Expr\Array_::KIND_SHORT) {
            return '[' . $this->pCommaSeparated($node->items) . ']';
        } else {
            return 'array(' . $this->pCommaSeparated($node->items) . ')';
        }
    }

    public function pExpr_ArrayItem(Expr\ArrayItem $node) {
        return (null !== $node->key ? $this->p($node->key) . ' => ' : '')
             . ($node->byRef ? '&' : '') . $this->p($node->value);
    }

    public function pExpr_ArrayDimFetch(Expr\ArrayDimFetch $node) {
        return $this->pDereferenceLhs($node->var)
             . '[' . (null !== $node->dim ? $this->p($node->dim) : '') . ']';
    }

    public function pExpr_ConstFetch(Expr\ConstFetch $node) {
        return $this->p($node->name);
    }

    public function pExpr_ClassConstFetch(Expr\ClassConstFetch $node) {
        return $this->p($node->class) . '::' . $node->name;
    }

    public function pExpr_PropertyFetch(Expr\PropertyFetch $node) {
        return $this->pDereferenceLhs($node->var) . '->' . $this->pObjectProperty($node->name);
    }

    public function pExpr_StaticPropertyFetch(Expr\StaticPropertyFetch $node) {
        return $this->pDereferenceLhs($node->class) . '::$' . $this->pObjectProperty($node->name);
    }

    public function pExpr_ShellExec(Expr\ShellExec $node) {
        return '`' . $this->pEncapsList($node->parts, '`') . '`';
    }

    public function pExpr_Closure(Expr\Closure $node) {
        return ($node->static ? 'static ' : '')
             . 'function ' . ($node->byRef ? '&' : '')
             . '(' . $this->pCommaSeparated($node->params) . ')'
             . (!empty($node->uses) ? ' use(' . $this->pCommaSeparated($node->uses) . ')': '')
             . (null !== $node->returnType ? ' : ' . $this->pType($node->returnType) : '')
             . ' {' . $this->pStmts($node->stmts) . "\n" . '}';
    }

    public function pExpr_ClosureUse(Expr\ClosureUse $node) {
        return ($node->byRef ? '&' : '') . '$' . $node->var;
    }

    public function pExpr_New(Expr\New_ $node) {
        if ($node->class instanceof Stmt\Class_) {
            $args = $node->args ? '(' . $this->pCommaSeparated($node->args) . ')' : '';
            return 'new ' . $this->pClassCommon($node->class, $args);
        }
        return 'new ' . $this->p($node->class) . '(' . $this->pCommaSeparated($node->args) . ')';
    }

    public function pExpr_Clone(Expr\Clone_ $node) {
        return 'clone ' . $this->p($node->expr);
    }

    public function pExpr_Ternary(Expr\Ternary $node) {
        // a bit of cheating: we treat the ternary as a binary op where the ?...: part is the operator.
        // this is okay because the part between ? and : never needs parentheses.
        return $this->pInfixOp('Expr_Ternary',
            $node->cond, ' ?' . (null !== $node->if ? ' ' . $this->p($node->if) . ' ' : '') . ': ', $node->else
        );
    }

    public function pExpr_Exit(Expr\Exit_ $node) {
        $kind = $node->getAttribute('kind', Expr\Exit_::KIND_DIE);
        return ($kind === Expr\Exit_::KIND_EXIT ? 'exit' : 'die')
             . (null !== $node->expr ? '(' . $this->p($node->expr) . ')' : '');
    }

    public function pExpr_Yield(Expr\Yield_ $node) {
        if ($node->value === null) {
            return 'yield';
        } else {
            // this is a bit ugly, but currently there is no way to detect whether the parentheses are necessary
            return '(yield '
                 . ($node->key !== null ? $this->p($node->key) . ' => ' : '')
                 . $this->p($node->value)
                 . ')';
        }
    }

    // Declarations

    public function pStmt_Namespace(Stmt\Namespace_ $node) {
        if ($this->canUseSemicolonNamespaces) {
            return 'namespace ' . $this->p($node->name) . ';' . "\n" . $this->pStmts($node->stmts, false);
        } else {
            return 'namespace' . (null !== $node->name ? ' ' . $this->p($node->name) : '')
                 . ' {' . $this->pStmts($node->stmts) . "\n" . '}';
        }
    }

    public function pStmt_Use(Stmt\Use_ $node) {
        return 'use ' . $this->pUseType($node->type)
             . $this->pCommaSeparated($node->uses) . ';';
    }

    public function pStmt_GroupUse(Stmt\GroupUse $node) {
        return 'use ' . $this->pUseType($node->type) . $this->pName($node->prefix)
             . '\{' . $this->pCommaSeparated($node->uses) . '};';
    }

    public function pStmt_UseUse(Stmt\UseUse $node) {
        return $this->pUseType($node->type) . $this->p($node->name)
             . ($node->name->getLast() !== $node->alias ? ' as ' . $node->alias : '');
    }

    private function pUseType($type) {
        return $type === Stmt\Use_::TYPE_FUNCTION ? 'function '
            : ($type === Stmt\Use_::TYPE_CONSTANT ? 'const ' : '');
    }

    public function pStmt_Interface(Stmt\Interface_ $node) {
        return 'interface ' . $node->name
             . (!empty($node->extends) ? ' extends ' . $this->pCommaSeparated($node->extends) : '')
             . "\n" . '{' . $this->pStmts($node->stmts) . "\n" . '}';
    }

    public function pStmt_Class(Stmt\Class_ $node) {
        return $this->pClassCommon($node, ' ' . $node->name);
    }

    public function pStmt_Trait(Stmt\Trait_ $node) {
        return 'trait ' . $node->name
             . "\n" . '{' . $this->pStmts($node->stmts) . "\n" . '}';
    }

    public function pStmt_TraitUse(Stmt\TraitUse $node) {
        return 'use ' . $this->pCommaSeparated($node->traits)
             . (empty($node->adaptations)
                ? ';'
                : ' {' . $this->pStmts($node->adaptations) . "\n" . '}');
    }

    public function pStmt_TraitUseAdaptation_Precedence(Stmt\TraitUseAdaptation\Precedence $node) {
        return $this->p($node->trait) . '::' . $node->method
             . ' insteadof ' . $this->pCommaSeparated($node->insteadof) . ';';
    }

    public function pStmt_TraitUseAdaptation_Alias(Stmt\TraitUseAdaptation\Alias $node) {
        return (null !== $node->trait ? $this->p($node->trait) . '::' : '')
             . $node->method . ' as'
             . (null !== $node->newModifier ? ' ' . rtrim($this->pModifiers($node->newModifier), ' ') : '')
             . (null !== $node->newName     ? ' ' . $node->newName                        : '')
             . ';';
    }

    public function pStmt_Property(Stmt\Property $node) {
        return (0 === $node->type ? 'var ' : $this->pModifiers($node->type)) . $this->pCommaSeparated($node->props) . ';';
    }

    public function pStmt_PropertyProperty(Stmt\PropertyProperty $node) {
        return '$' . $node->name
             . (null !== $node->default ? ' = ' . $this->p($node->default) : '');
    }

    public function pStmt_ClassMethod(Stmt\ClassMethod $node) {
        return $this->pModifiers($node->type)
             . 'function ' . ($node->byRef ? '&' : '') . $node->name
             . '(' . $this->pCommaSeparated($node->params) . ')'
             . (null !== $node->returnType ? ' : ' . $this->pType($node->returnType) : '')
             . (null !== $node->stmts
                ? "\n" . '{' . $this->pStmts($node->stmts) . "\n" . '}'
                : ';');
    }

    public function pStmt_ClassConst(Stmt\ClassConst $node) {
        return 'const ' . $this->pCommaSeparated($node->consts) . ';';
    }

    public function pStmt_Function(Stmt\Function_ $node) {
        return 'function ' . ($node->byRef ? '&' : '') . $node->name
             . '(' . $this->pCommaSeparated($node->params) . ')'
             . (null !== $node->returnType ? ' : ' . $this->pType($node->returnType) : '')
             . "\n" . '{' . $this->pStmts($node->stmts) . "\n" . '}';
    }

    public function pStmt_Const(Stmt\Const_ $node) {
        return 'const ' . $this->pCommaSeparated($node->consts) . ';';
    }

    public function pStmt_Declare(Stmt\Declare_ $node) {
        return 'declare (' . $this->pCommaSeparated($node->declares) . ')'
             . (null !== $node->stmts ? ' {' . $this->pStmts($node->stmts) . "\n" . '}' : ';');
    }

    public function pStmt_DeclareDeclare(Stmt\DeclareDeclare $node) {
        return $node->key . '=' . $this->p($node->value);
    }

    // Control flow

    public function pStmt_If(Stmt\If_ $node) {
        return 'if (' . $this->p($node->cond) . ') {'
             . $this->pStmts($node->stmts) . "\n" . '}'
             . $this->pImplode($node->elseifs)
             . (null !== $node->else ? $this->p($node->else) : '');
    }

    public function pStmt_ElseIf(Stmt\ElseIf_ $node) {
        return ' elseif (' . $this->p($node->cond) . ') {'
             . $this->pStmts($node->stmts) . "\n" . '}';
    }

    public function pStmt_Else(Stmt\Else_ $node) {
        return ' else {' . $this->pStmts($node->stmts) . "\n" . '}';
    }

    public function pStmt_For(Stmt\For_ $node) {
        return 'for ('
             . $this->pCommaSeparated($node->init) . ';' . (!empty($node->cond) ? ' ' : '')
             . $this->pCommaSeparated($node->cond) . ';' . (!empty($node->loop) ? ' ' : '')
             . $this->pCommaSeparated($node->loop)
             . ') {' . $this->pStmts($node->stmts) . "\n" . '}';
    }

    public function pStmt_Foreach(Stmt\Foreach_ $node) {
        return 'foreach (' . $this->p($node->expr) . ' as '
             . (null !== $node->keyVar ? $this->p($node->keyVar) . ' => ' : '')
             . ($node->byRef ? '&' : '') . $this->p($node->valueVar) . ') {'
             . $this->pStmts($node->stmts) . "\n" . '}';
    }

    public function pStmt_While(Stmt\While_ $node) {
        return 'while (' . $this->p($node->cond) . ') {'
             . $this->pStmts($node->stmts) . "\n" . '}';
    }

    public function pStmt_Do(Stmt\Do_ $node) {
        return 'do {' . $this->pStmts($node->stmts) . "\n"
             . '} while (' . $this->p($node->cond) . ');';
    }

    public function pStmt_Switch(Stmt\Switch_ $node) {
        return 'switch (' . $this->p($node->cond) . ') {'
             . $this->pStmts($node->cases) . "\n" . '}';
    }

    public function pStmt_Case(Stmt\Case_ $node) {
        return (null !== $node->cond ? 'case ' . $this->p($node->cond) : 'default') . ':'
             . $this->pStmts($node->stmts);
    }

    public function pStmt_TryCatch(Stmt\TryCatch $node) {
        return 'try {' . $this->pStmts($node->stmts) . "\n" . '}'
             . $this->pImplode($node->catches)
             . ($node->finallyStmts !== null
                ? ' finally {' . $this->pStmts($node->finallyStmts) . "\n" . '}'
                : '');
    }

    public function pStmt_Catch(Stmt\Catch_ $node) {
        return ' catch (' . $this->p($node->type) . ' $' . $node->var . ') {'
             . $this->pStmts($node->stmts) . "\n" . '}';
    }

    public function pStmt_Break(Stmt\Break_ $node) {
        return 'break' . ($node->num !== null ? ' ' . $this->p($node->num) : '') . ';';
    }

    public function pStmt_Continue(Stmt\Continue_ $node) {
        return 'continue' . ($node->num !== null ? ' ' . $this->p($node->num) : '') . ';';
    }

    public function pStmt_Return(Stmt\Return_ $node) {
        return 'return' . (null !== $node->expr ? ' ' . $this->p($node->expr) : '') . ';';
    }

    public function pStmt_Throw(Stmt\Throw_ $node) {
        return 'throw ' . $this->p($node->expr) . ';';
    }

    public function pStmt_Label(Stmt\Label $node) {
        return $node->name . ':';
    }

    public function pStmt_Goto(Stmt\Goto_ $node) {
        return 'goto ' . $node->name . ';';
    }

    // Other

    public function pStmt_Echo(Stmt\Echo_ $node) {
        return 'echo ' . $this->pCommaSeparated($node->exprs) . ';';
    }

    public function pStmt_Static(Stmt\Static_ $node) {
        return 'static ' . $this->pCommaSeparated($node->vars) . ';';
    }

    public function pStmt_Global(Stmt\Global_ $node) {
        return 'global ' . $this->pCommaSeparated($node->vars) . ';';
    }

    public function pStmt_StaticVar(Stmt\StaticVar $node) {
        return '$' . $node->name
             . (null !== $node->default ? ' = ' . $this->p($node->default) : '');
    }

    public function pStmt_Unset(Stmt\Unset_ $node) {
        return 'unset(' . $this->pCommaSeparated($node->vars) . ');';
    }

    public function pStmt_InlineHTML(Stmt\InlineHTML $node) {
        return '?>' . $this->pNoIndent("\n" . $node->value) . 'remaining;
    }

    public function pStmt_Nop(Stmt\Nop $node) {
        return '';
    }

    // Helpers

    protected function pType($node) {
        return is_string($node) ? $node : $this->p($node);
    }

    protected function pClassCommon(Stmt\Class_ $node, $afterClassToken) {
        return $this->pModifiers($node->type)
        . 'class' . $afterClassToken
        . (null !== $node->extends ? ' extends ' . $this->p($node->extends) : '')
        . (!empty($node->implements) ? ' implements ' . $this->pCommaSeparated($node->implements) : '')
        . "\n" . '{' . $this->pStmts($node->stmts) . "\n" . '}';
    }

    protected function pObjectProperty($node) {
        if ($node instanceof Expr) {
            return '{' . $this->p($node) . '}';
        } else {
            return $node;
        }
    }

    protected function pModifiers($modifiers) {
        return ($modifiers & Stmt\Class_::MODIFIER_PUBLIC    ? 'public '    : '')
             . ($modifiers & Stmt\Class_::MODIFIER_PROTECTED ? 'protected ' : '')
             . ($modifiers & Stmt\Class_::MODIFIER_PRIVATE   ? 'private '   : '')
             . ($modifiers & Stmt\Class_::MODIFIER_STATIC    ? 'static '    : '')
             . ($modifiers & Stmt\Class_::MODIFIER_ABSTRACT  ? 'abstract '  : '')
             . ($modifiers & Stmt\Class_::MODIFIER_FINAL     ? 'final '     : '');
    }

    protected function pEncapsList(array $encapsList, $quote) {
        $return = '';
        foreach ($encapsList as $element) {
            if ($element instanceof Scalar\EncapsedStringPart) {
                $return .= $this->escapeString($element->value, $quote);
            } else {
                $return .= '{' . $this->p($element) . '}';
            }
        }

        return $return;
    }

    protected function escapeString($string, $quote) {
        if (null === $quote) {
            // For doc strings, don't escape newlines
            $escaped = addcslashes($string, "\t\f\v$\\");
        } else {
            $escaped = addcslashes($string, "\n\r\t\f\v$" . $quote . "\\");
        }

        // Escape other control characters
        return preg_replace_callback('/([\0-\10\16-\37])(?=([0-7]?))/', function ($matches) {
            $oct = decoct(ord($matches[1]));
            if ($matches[2] !== '') {
                // If there is a trailing digit, use the full three character form
                return '\\' . str_pad($oct, 3, '0', STR_PAD_LEFT);
            }
            return '\\' . $oct;
        }, $escaped);
    }

    protected function containsEndLabel($string, $label, $atStart = true, $atEnd = true) {
        $start = $atStart ? '(?:^|[\r\n])' : '[\r\n]';
        $end = $atEnd ? '(?:$|[;\r\n])' : '[;\r\n]';
        return false !== strpos($string, $label)
            && preg_match('/' . $start . $label . $end . '/', $string);
    }

    protected function encapsedContainsEndLabel(array $parts, $label) {
        foreach ($parts as $i => $part) {
            $atStart = $i === 0;
            $atEnd = $i === count($parts) - 1;
            if ($part instanceof Scalar\EncapsedStringPart
                && $this->containsEndLabel($part->value, $label, $atStart, $atEnd)
            ) {
                return true;
            }
        }
        return false;
    }

    protected function pDereferenceLhs(Node $node) {
        if ($node instanceof Expr\Variable
            || $node instanceof Name
            || $node instanceof Expr\ArrayDimFetch
            || $node instanceof Expr\PropertyFetch
            || $node instanceof Expr\StaticPropertyFetch
            || $node instanceof Expr\FuncCall
            || $node instanceof Expr\MethodCall
            || $node instanceof Expr\StaticCall
            || $node instanceof Expr\Array_
            || $node instanceof Scalar\String_
            || $node instanceof Expr\ConstFetch
            || $node instanceof Expr\ClassConstFetch
        ) {
            return $this->p($node);
        } else  {
            return '(' . $this->p($node) . ')';
        }
    }

    protected function pCallLhs(Node $node) {
        if ($node instanceof Name
            || $node instanceof Expr\Variable
            || $node instanceof Expr\ArrayDimFetch
            || $node instanceof Expr\FuncCall
            || $node instanceof Expr\MethodCall
            || $node instanceof Expr\StaticCall
            || $node instanceof Expr\Array_
        ) {
            return $this->p($node);
        } else  {
            return '(' . $this->p($node) . ')';
        }
    }
}
 array(  0,  1),
        'Expr_BitwiseNot'              => array( 10,  1),
        'Expr_PreInc'                  => array( 10,  1),
        'Expr_PreDec'                  => array( 10,  1),
        'Expr_PostInc'                 => array( 10, -1),
        'Expr_PostDec'                 => array( 10, -1),
        'Expr_UnaryPlus'               => array( 10,  1),
        'Expr_UnaryMinus'              => array( 10,  1),
        'Expr_Cast_Int'                => array( 10,  1),
        'Expr_Cast_Double'             => array( 10,  1),
        'Expr_Cast_String'             => array( 10,  1),
        'Expr_Cast_Array'              => array( 10,  1),
        'Expr_Cast_Object'             => array( 10,  1),
        'Expr_Cast_Bool'               => array( 10,  1),
        'Expr_Cast_Unset'              => array( 10,  1),
        'Expr_ErrorSuppress'           => array( 10,  1),
        'Expr_Instanceof'              => array( 20,  0),
        'Expr_BooleanNot'              => array( 30,  1),
        'Expr_BinaryOp_Mul'            => array( 40, -1),
        'Expr_BinaryOp_Div'            => array( 40, -1),
        'Expr_BinaryOp_Mod'            => array( 40, -1),
        'Expr_BinaryOp_Plus'           => array( 50, -1),
        'Expr_BinaryOp_Minus'          => array( 50, -1),
        'Expr_BinaryOp_Concat'         => array( 50, -1),
        'Expr_BinaryOp_ShiftLeft'      => array( 60, -1),
        'Expr_BinaryOp_ShiftRight'     => array( 60, -1),
        'Expr_BinaryOp_Smaller'        => array( 70,  0),
        'Expr_BinaryOp_SmallerOrEqual' => array( 70,  0),
        'Expr_BinaryOp_Greater'        => array( 70,  0),
        'Expr_BinaryOp_GreaterOrEqual' => array( 70,  0),
        'Expr_BinaryOp_Equal'          => array( 80,  0),
        'Expr_BinaryOp_NotEqual'       => array( 80,  0),
        'Expr_BinaryOp_Identical'      => array( 80,  0),
        'Expr_BinaryOp_NotIdentical'   => array( 80,  0),
        'Expr_BinaryOp_Spaceship'      => array( 80,  0),
        'Expr_BinaryOp_BitwiseAnd'     => array( 90, -1),
        'Expr_BinaryOp_BitwiseXor'     => array(100, -1),
        'Expr_BinaryOp_BitwiseOr'      => array(110, -1),
        'Expr_BinaryOp_BooleanAnd'     => array(120, -1),
        'Expr_BinaryOp_BooleanOr'      => array(130, -1),
        'Expr_BinaryOp_Coalesce'       => array(140,  1),
        'Expr_Ternary'                 => array(150, -1),
        // parser uses %left for assignments, but they really behave as %right
        'Expr_Assign'                  => array(160,  1),
        'Expr_AssignRef'               => array(160,  1),
        'Expr_AssignOp_Plus'           => array(160,  1),
        'Expr_AssignOp_Minus'          => array(160,  1),
        'Expr_AssignOp_Mul'            => array(160,  1),
        'Expr_AssignOp_Div'            => array(160,  1),
        'Expr_AssignOp_Concat'         => array(160,  1),
        'Expr_AssignOp_Mod'            => array(160,  1),
        'Expr_AssignOp_BitwiseAnd'     => array(160,  1),
        'Expr_AssignOp_BitwiseOr'      => array(160,  1),
        'Expr_AssignOp_BitwiseXor'     => array(160,  1),
        'Expr_AssignOp_ShiftLeft'      => array(160,  1),
        'Expr_AssignOp_ShiftRight'     => array(160,  1),
        'Expr_AssignOp_Pow'            => array(160,  1),
        'Expr_YieldFrom'               => array(165,  1),
        'Expr_Print'                   => array(168,  1),
        'Expr_BinaryOp_LogicalAnd'     => array(170, -1),
        'Expr_BinaryOp_LogicalXor'     => array(180, -1),
        'Expr_BinaryOp_LogicalOr'      => array(190, -1),
        'Expr_Include'                 => array(200, -1),
    );

    protected $noIndentToken;
    protected $docStringEndToken;
    protected $canUseSemicolonNamespaces;
    protected $options;

    /**
     * Creates a pretty printer instance using the given options.
     *
     * Supported options:
     *  * bool $shortArraySyntax = false: Whether to use [] instead of array() as the default array
     *                                    syntax, if the node does not specify a format.
     *
     * @param array $options Dictionary of formatting options
     */
    public function __construct(array $options = []) {
        $this->noIndentToken = '_NO_INDENT_' . mt_rand();
        $this->docStringEndToken = '_DOC_STRING_END_' . mt_rand();

        $defaultOptions = ['shortArraySyntax' => false];
        $this->options = $options + $defaultOptions;
    }

    /**
     * Pretty prints an array of statements.
     *
     * @param Node[] $stmts Array of statements
     *
     * @return string Pretty printed statements
     */
    public function prettyPrint(array $stmts) {
        $this->preprocessNodes($stmts);

        return ltrim($this->handleMagicTokens($this->pStmts($stmts, false)));
    }

    /**
     * Pretty prints an expression.
     *
     * @param Expr $node Expression node
     *
     * @return string Pretty printed node
     */
    public function prettyPrintExpr(Expr $node) {
        return $this->handleMagicTokens($this->p($node));
    }

    /**
     * Pretty prints a file of statements (includes the opening prettyPrint($stmts);

        if ($stmts[0] instanceof Stmt\InlineHTML) {
            $p = preg_replace('/^<\?php\s+\?>\n?/', '', $p);
        }
        if ($stmts[count($stmts) - 1] instanceof Stmt\InlineHTML) {
            $p = preg_replace('/<\?php$/', '', rtrim($p));
        }

        return $p;
    }

    /**
     * Preprocesses the top-level nodes to initialize pretty printer state.
     *
     * @param Node[] $nodes Array of nodes
     */
    protected function preprocessNodes(array $nodes) {
        /* We can use semicolon-namespaces unless there is a global namespace declaration */
        $this->canUseSemicolonNamespaces = true;
        foreach ($nodes as $node) {
            if ($node instanceof Stmt\Namespace_ && null === $node->name) {
                $this->canUseSemicolonNamespaces = false;
            }
        }
    }

    protected function handleMagicTokens($str) {
        // Drop no-indent tokens
        $str = str_replace($this->noIndentToken, '', $str);

        // Replace doc-string-end tokens with nothing or a newline
        $str = str_replace($this->docStringEndToken . ";\n", ";\n", $str);
        $str = str_replace($this->docStringEndToken, "\n", $str);

        return $str;
    }

    /**
     * Pretty prints an array of nodes (statements) and indents them optionally.
     *
     * @param Node[] $nodes  Array of nodes
     * @param bool   $indent Whether to indent the printed nodes
     *
     * @return string Pretty printed statements
     */
    protected function pStmts(array $nodes, $indent = true) {
        $result = '';
        foreach ($nodes as $node) {
            $comments = $node->getAttribute('comments', array());
            if ($comments) {
                $result .= "\n" . $this->pComments($comments);
                if ($node instanceof Stmt\Nop) {
                    continue;
                }
            }

            $result .= "\n" . $this->p($node) . ($node instanceof Expr ? ';' : '');
        }

        if ($indent) {
            return preg_replace('~\n(?!$|' . $this->noIndentToken . ')~', "\n    ", $result);
        } else {
            return $result;
        }
    }

    /**
     * Pretty prints a node.
     *
     * @param Node $node Node to be pretty printed
     *
     * @return string Pretty printed node
     */
    protected function p(Node $node) {
        return $this->{'p' . $node->getType()}($node);
    }

    protected function pInfixOp($type, Node $leftNode, $operatorString, Node $rightNode) {
        list($precedence, $associativity) = $this->precedenceMap[$type];

        return $this->pPrec($leftNode, $precedence, $associativity, -1)
             . $operatorString
             . $this->pPrec($rightNode, $precedence, $associativity, 1);
    }

    protected function pPrefixOp($type, $operatorString, Node $node) {
        list($precedence, $associativity) = $this->precedenceMap[$type];
        return $operatorString . $this->pPrec($node, $precedence, $associativity, 1);
    }

    protected function pPostfixOp($type, Node $node, $operatorString) {
        list($precedence, $associativity) = $this->precedenceMap[$type];
        return $this->pPrec($node, $precedence, $associativity, -1) . $operatorString;
    }

    /**
     * Prints an expression node with the least amount of parentheses necessary to preserve the meaning.
     *
     * @param Node $node                Node to pretty print
     * @param int  $parentPrecedence    Precedence of the parent operator
     * @param int  $parentAssociativity Associativity of parent operator
     *                                  (-1 is left, 0 is nonassoc, 1 is right)
     * @param int  $childPosition       Position of the node relative to the operator
     *                                  (-1 is left, 1 is right)
     *
     * @return string The pretty printed node
     */
    protected function pPrec(Node $node, $parentPrecedence, $parentAssociativity, $childPosition) {
        $type = $node->getType();
        if (isset($this->precedenceMap[$type])) {
            $childPrecedence = $this->precedenceMap[$type][0];
            if ($childPrecedence > $parentPrecedence
                || ($parentPrecedence == $childPrecedence && $parentAssociativity != $childPosition)
            ) {
                return '(' . $this->{'p' . $type}($node) . ')';
            }
        }

        return $this->{'p' . $type}($node);
    }

    /**
     * Pretty prints an array of nodes and implodes the printed values.
     *
     * @param Node[] $nodes Array of Nodes to be printed
     * @param string $glue  Character to implode with
     *
     * @return string Imploded pretty printed nodes
     */
    protected function pImplode(array $nodes, $glue = '') {
        $pNodes = array();
        foreach ($nodes as $node) {
            $pNodes[] = $this->p($node);
        }

        return implode($glue, $pNodes);
    }

    /**
     * Pretty prints an array of nodes and implodes the printed values with commas.
     *
     * @param Node[] $nodes Array of Nodes to be printed
     *
     * @return string Comma separated pretty printed nodes
     */
    protected function pCommaSeparated(array $nodes) {
        return $this->pImplode($nodes, ', ');
    }

    /**
     * Signals the pretty printer that a string shall not be indented.
     *
     * @param string $string Not to be indented string
     *
     * @return string String marked with $this->noIndentToken's.
     */
    protected function pNoIndent($string) {
        return str_replace("\n", "\n" . $this->noIndentToken, $string);
    }

    /**
     * Prints reformatted text of the passed comments.
     *
     * @param Comment[] $comments List of comments
     *
     * @return string Reformatted text of comments
     */
    protected function pComments(array $comments) {
        $formattedComments = [];

        foreach ($comments as $comment) {
            $formattedComments[] = $comment->getReformattedText();
        }

        return implode("\n", $formattedComments);
    }
}
writer = new XMLWriter;
        $this->writer->openMemory();
        $this->writer->setIndent(true);
    }

    public function serialize(array $nodes) {
        $this->writer->flush();
        $this->writer->startDocument('1.0', 'UTF-8');

        $this->writer->startElement('AST');
        $this->writer->writeAttribute('xmlns:node',      'http://nikic.github.com/PHPParser/XML/node');
        $this->writer->writeAttribute('xmlns:subNode',   'http://nikic.github.com/PHPParser/XML/subNode');
        $this->writer->writeAttribute('xmlns:attribute', 'http://nikic.github.com/PHPParser/XML/attribute');
        $this->writer->writeAttribute('xmlns:scalar',    'http://nikic.github.com/PHPParser/XML/scalar');

        $this->_serialize($nodes);

        $this->writer->endElement();

        return $this->writer->outputMemory();
    }

    protected function _serialize($node) {
        if ($node instanceof Node) {
            $this->writer->startElement('node:' . $node->getType());

            foreach ($node->getAttributes() as $name => $value) {
                $this->writer->startElement('attribute:' . $name);
                $this->_serialize($value);
                $this->writer->endElement();
            }

            foreach ($node as $name => $subNode) {
                $this->writer->startElement('subNode:' . $name);
                $this->_serialize($subNode);
                $this->writer->endElement();
            }

            $this->writer->endElement();
        } elseif ($node instanceof Comment) {
            $this->writer->startElement('comment');
            $this->writer->writeAttribute('isDocComment', $node instanceof Comment\Doc ? 'true' : 'false');
            $this->writer->writeAttribute('line', (string) $node->getLine());
            $this->writer->text($node->getText());
            $this->writer->endElement();
        } elseif (is_array($node)) {
            $this->writer->startElement('scalar:array');
            foreach ($node as $subNode) {
                $this->_serialize($subNode);
            }
            $this->writer->endElement();
        } elseif (is_string($node)) {
            $this->writer->writeElement('scalar:string', $node);
        } elseif (is_int($node)) {
            $this->writer->writeElement('scalar:int', (string) $node);
        } elseif (is_float($node)) {
            // TODO Higher precision conversion?
            $this->writer->writeElement('scalar:float', (string) $node);
        } elseif (true === $node) {
            $this->writer->writeElement('scalar:true');
        } elseif (false === $node) {
            $this->writer->writeElement('scalar:false');
        } elseif (null === $node) {
            $this->writer->writeElement('scalar:null');
        } else {
            throw new \InvalidArgumentException('Unexpected node type');
        }
    }
}
reader = new XMLReader;
    }

    public function unserialize($string) {
        $this->reader->XML($string);

        $this->reader->read();
        if ('AST' !== $this->reader->name) {
            throw new DomainException('AST root element not found');
        }

        return $this->read($this->reader->depth);
    }

    protected function read($depthLimit, $throw = true, &$nodeFound = null) {
        $nodeFound = true;
        while ($this->reader->read() && $depthLimit < $this->reader->depth) {
            if (XMLReader::ELEMENT !== $this->reader->nodeType) {
                continue;
            }

            if ('node' === $this->reader->prefix) {
                return $this->readNode();
            } elseif ('scalar' === $this->reader->prefix) {
                return $this->readScalar();
            } elseif ('comment' === $this->reader->name) {
                return $this->readComment();
            } else {
                throw new DomainException(sprintf('Unexpected node of type "%s"', $this->reader->name));
            }
        }

        $nodeFound = false;
        if ($throw) {
            throw new DomainException('Expected node or scalar');
        }
    }

    protected function readNode() {
        $className = $this->getClassNameFromType($this->reader->localName);

        // create the node without calling it's constructor
        $node = unserialize(
            sprintf(
                "O:%d:\"%s\":1:{s:13:\"\0*\0attributes\";a:0:{}}",
                strlen($className), $className
            )
        );

        $depthLimit = $this->reader->depth;
        while ($this->reader->read() && $depthLimit < $this->reader->depth) {
            if (XMLReader::ELEMENT !== $this->reader->nodeType) {
                continue;
            }

            $type = $this->reader->prefix;
            if ('subNode' !== $type && 'attribute' !== $type) {
                throw new DomainException(
                    sprintf('Expected sub node or attribute, got node of type "%s"', $this->reader->name)
                );
            }

            $name = $this->reader->localName;
            $value = $this->read($this->reader->depth);

            if ('subNode' === $type) {
                $node->$name = $value;
            } else {
                $node->setAttribute($name, $value);
            }
        }

        return $node;
    }

    protected function readScalar() {
        switch ($name = $this->reader->localName) {
            case 'array':
                $depth = $this->reader->depth;
                $array = array();
                while (true) {
                    $node = $this->read($depth, false, $nodeFound);
                    if (!$nodeFound) {
                        break;
                    }
                    $array[] = $node;
                }
                return $array;
            case 'string':
                return $this->reader->readString();
            case 'int':
                return $this->parseInt($this->reader->readString());
            case 'float':
                $text = $this->reader->readString();
                if (false === $float = filter_var($text, FILTER_VALIDATE_FLOAT)) {
                    throw new DomainException(sprintf('"%s" is not a valid float', $text));
                }
                return $float;
            case 'true':
            case 'false':
            case 'null':
                if (!$this->reader->isEmptyElement) {
                    throw new DomainException(sprintf('"%s" scalar must be empty', $name));
                }
                return constant($name);
            default:
                throw new DomainException(sprintf('Unknown scalar type "%s"', $name));
        }
    }

    private function parseInt($text) {
        if (false === $int = filter_var($text, FILTER_VALIDATE_INT)) {
            throw new DomainException(sprintf('"%s" is not a valid integer', $text));
        }
        return $int;
    }

    protected function readComment() {
        $className = $this->reader->getAttribute('isDocComment') === 'true'
            ? 'PhpParser\Comment\Doc'
            : 'PhpParser\Comment'
        ;
        return new $className(
            $this->reader->readString(),
            $this->parseInt($this->reader->getAttribute('line'))
        );
    }

    protected function getClassNameFromType($type) {
        $className = 'PhpParser\\Node\\' . strtr($type, '_', '\\');
        if (!class_exists($className)) {
            $className .= '_';
        }
        if (!class_exists($className)) {
            throw new DomainException(sprintf('Unknown node type "%s"', $type));
        }
        return $className;
    }
}
parse($code);
        $parseTime += microtime(true) - $startTime;

        $startTime = microtime(true);
        $code = 'prettyPrint($stmts);
        $ppTime += microtime(true) - $startTime;

        try {
            $startTime = microtime(true);
            $ppStmts = $parser->parse($code);
            $reparseTime += microtime(true) - $startTime;

            $startTime = microtime(true);
            $same = $nodeDumper->dump($stmts) == $nodeDumper->dump($ppStmts);
            $compareTime += microtime(true) - $startTime;

            if (!$same) {
                echo $file, ":\n    Result of initial parse and parse after pretty print differ\n";
                if ($verbose) {
                    echo "Pretty printer output:\n=====\n$code\n=====\n\n";
                }

                ++$compareFail;
            }
        } catch (PhpParser\Error $e) {
            echo $file, ":\n    Parse of pretty print failed with message: {$e->getMessage()}\n";
            if ($verbose) {
                echo "Pretty printer output:\n=====\n$code\n=====\n\n";
            }

            ++$ppFail;
        }
    } catch (PhpParser\Error $e) {
        echo $file, ":\n    Parse failed with message: {$e->getMessage()}\n";

        ++$parseFail;
    }
}

if (0 === $parseFail && 0 === $ppFail && 0 === $compareFail) {
    $exit = 0;
    echo "\n\n", 'All tests passed.', "\n";
} else {
    $exit = 1;
    echo "\n\n", '==========', "\n\n", 'There were: ', "\n";
    if (0 !== $parseFail) {
        echo '    ', $parseFail,   ' parse failures.',        "\n";
    }
    if (0 !== $ppFail) {
        echo '    ', $ppFail,      ' pretty print failures.', "\n";
    }
    if (0 !== $compareFail) {
        echo '    ', $compareFail, ' compare failures.',      "\n";
    }
}

echo "\n",
     'Tested files:         ', $count,        "\n",
     "\n",
     'Reading files took:   ', $readTime,    "\n",
     'Parsing took:         ', $parseTime,   "\n",
     'Pretty printing took: ', $ppTime,      "\n",
     'Reparsing took:       ', $reparseTime, "\n",
     'Comparing took:       ', $compareTime, "\n",
     "\n",
     'Total time:           ', microtime(true) - $totalStartTime, "\n",
     'Maximum memory usage: ', memory_get_peak_usage(true), "\n";

exit($exit);

 * @author    Jan Schneider 
 * @copyright 2002-2005 Richard Heyes
 * @copyright 2006-2008 Jan Schneider
 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
 * @version   CVS: $Id$
 * @link      http://pear.php.net/package/Console_Table
 */

define('CONSOLE_TABLE_HORIZONTAL_RULE', 1);
define('CONSOLE_TABLE_ALIGN_LEFT', -1);
define('CONSOLE_TABLE_ALIGN_CENTER', 0);
define('CONSOLE_TABLE_ALIGN_RIGHT', 1);
define('CONSOLE_TABLE_BORDER_ASCII', -1);

/**
 * The main class.
 *
 * @category Console
 * @package  Console_Table
 * @author   Jan Schneider 
 * @license  http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
 * @link     http://pear.php.net/package/Console_Table
 */
class Console_Table
{
    /**
     * The table headers.
     *
     * @var array
     */
    var $_headers = array();

    /**
     * The data of the table.
     *
     * @var array
     */
    var $_data = array();

    /**
     * The maximum number of columns in a row.
     *
     * @var integer
     */
    var $_max_cols = 0;

    /**
     * The maximum number of rows in the table.
     *
     * @var integer
     */
    var $_max_rows = 0;

    /**
     * Lengths of the columns, calculated when rows are added to the table.
     *
     * @var array
     */
    var $_cell_lengths = array();

    /**
     * Heights of the rows.
     *
     * @var array
     */
    var $_row_heights = array();

    /**
     * How many spaces to use to pad the table.
     *
     * @var integer
     */
    var $_padding = 1;

    /**
     * Column filters.
     *
     * @var array
     */
    var $_filters = array();

    /**
     * Columns to calculate totals for.
     *
     * @var array
     */
    var $_calculateTotals;

    /**
     * Alignment of the columns.
     *
     * @var array
     */
    var $_col_align = array();

    /**
     * Default alignment of columns.
     *
     * @var integer
     */
    var $_defaultAlign;

    /**
     * Character set of the data.
     *
     * @var string
     */
    var $_charset = 'utf-8';

    /**
     * Border characters.
     * Allowed keys:
     * - intersection - intersection ("+")
     * - horizontal - horizontal rule character ("-")
     * - vertical - vertical rule character ("|")
     *
     * @var array
     */
    var $_border = array(
        'intersection' => '+',
        'horizontal' => '-',
        'vertical' => '|',
    );

    /**
     * If borders are shown or not
     * Allowed keys: top, right, bottom, left, inner: true and false
     *
     * @var array
     */
    var $_borderVisibility = array(
        'top'    => true,
        'right'  => true,
        'bottom' => true,
        'left'   => true,
        'inner'  => true
    );

    /**
     * Whether the data has ANSI colors.
     *
     * @var Console_Color2
     */
    var $_ansiColor = false;

    /**
     * Constructor.
     *
     * @param integer $align   Default alignment. One of
     *                         CONSOLE_TABLE_ALIGN_LEFT,
     *                         CONSOLE_TABLE_ALIGN_CENTER or
     *                         CONSOLE_TABLE_ALIGN_RIGHT.
     * @param string  $border  The character used for table borders or
     *                         CONSOLE_TABLE_BORDER_ASCII.
     * @param integer $padding How many spaces to use to pad the table.
     * @param string  $charset A charset supported by the mbstring PHP
     *                         extension.
     * @param boolean $color   Whether the data contains ansi color codes.
     */
    function __construct($align = CONSOLE_TABLE_ALIGN_LEFT,
                         $border = CONSOLE_TABLE_BORDER_ASCII, $padding = 1,
                         $charset = null, $color = false)
    {
        $this->_defaultAlign = $align;
        $this->setBorder($border);
        $this->_padding      = $padding;
        if ($color) {
            if (!class_exists('Console_Color2')) {
                include_once 'Console/Color2.php';
            }
            $this->_ansiColor = new Console_Color2();
        }
        if (!empty($charset)) {
            $this->setCharset($charset);
        }
    }

    /**
     * Converts an array to a table.
     *
     * @param array   $headers      Headers for the table.
     * @param array   $data         A two dimensional array with the table
     *                              data.
     * @param boolean $returnObject Whether to return the Console_Table object
     *                              instead of the rendered table.
     *
     * @static
     *
     * @return Console_Table|string  A Console_Table object or the generated
     *                               table.
     */
    function fromArray($headers, $data, $returnObject = false)
    {
        if (!is_array($headers) || !is_array($data)) {
            return false;
        }

        $table = new Console_Table();
        $table->setHeaders($headers);

        foreach ($data as $row) {
            $table->addRow($row);
        }

        return $returnObject ? $table : $table->getTable();
    }

    /**
     * Adds a filter to a column.
     *
     * Filters are standard PHP callbacks which are run on the data before
     * table generation is performed. Filters are applied in the order they
     * are added. The callback function must accept a single argument, which
     * is a single table cell.
     *
     * @param integer $col       Column to apply filter to.
     * @param mixed   &$callback PHP callback to apply.
     *
     * @return void
     */
    function addFilter($col, &$callback)
    {
        $this->_filters[] = array($col, &$callback);
    }

    /**
     * Sets the charset of the provided table data.
     *
     * @param string $charset A charset supported by the mbstring PHP
     *                        extension.
     *
     * @return void
     */
    function setCharset($charset)
    {
        $locale = setlocale(LC_CTYPE, 0);
        setlocale(LC_CTYPE, 'en_US');
        $this->_charset = strtolower($charset);
        setlocale(LC_CTYPE, $locale);
    }

    /**
     * Set the table border settings
     *
     * Border definition modes:
     * - CONSOLE_TABLE_BORDER_ASCII: Default border with +, - and |
     * - array with keys "intersection", "horizontal" and "vertical"
     * - single character string that sets all three of the array keys
     *
     * @param mixed $border Border definition
     *
     * @return void
     * @see $_border
     */
    function setBorder($border)
    {
        if ($border === CONSOLE_TABLE_BORDER_ASCII) {
            $intersection = '+';
            $horizontal = '-';
            $vertical = '|';
        } else if (is_string($border)) {
            $intersection = $horizontal = $vertical = $border;
        } else if ($border == '') {
            $intersection = $horizontal = $vertical = '';
        } else {
            extract($border);
        }

        $this->_border = array(
            'intersection' => $intersection,
            'horizontal' => $horizontal,
            'vertical' => $vertical,
        );
    }

    /**
     * Set which borders shall be shown.
     *
     * @param array $visibility Visibility settings.
     *                          Allowed keys: left, right, top, bottom, inner
     *
     * @return void
     * @see    $_borderVisibility
     */
    function setBorderVisibility($visibility)
    {
        $this->_borderVisibility = array_merge(
            $this->_borderVisibility,
            array_intersect_key(
                $visibility,
                $this->_borderVisibility
            )
        );
    }

    /**
     * Sets the alignment for the columns.
     *
     * @param integer $col_id The column number.
     * @param integer $align  Alignment to set for this column. One of
     *                        CONSOLE_TABLE_ALIGN_LEFT
     *                        CONSOLE_TABLE_ALIGN_CENTER
     *                        CONSOLE_TABLE_ALIGN_RIGHT.
     *
     * @return void
     */
    function setAlign($col_id, $align = CONSOLE_TABLE_ALIGN_LEFT)
    {
        switch ($align) {
        case CONSOLE_TABLE_ALIGN_CENTER:
            $pad = STR_PAD_BOTH;
            break;
        case CONSOLE_TABLE_ALIGN_RIGHT:
            $pad = STR_PAD_LEFT;
            break;
        default:
            $pad = STR_PAD_RIGHT;
            break;
        }
        $this->_col_align[$col_id] = $pad;
    }

    /**
     * Specifies which columns are to have totals calculated for them and
     * added as a new row at the bottom.
     *
     * @param array $cols Array of column numbers (starting with 0).
     *
     * @return void
     */
    function calculateTotalsFor($cols)
    {
        $this->_calculateTotals = $cols;
    }

    /**
     * Sets the headers for the columns.
     *
     * @param array $headers The column headers.
     *
     * @return void
     */
    function setHeaders($headers)
    {
        $this->_headers = array(array_values($headers));
        $this->_updateRowsCols($headers);
    }

    /**
     * Adds a row to the table.
     *
     * @param array   $row    The row data to add.
     * @param boolean $append Whether to append or prepend the row.
     *
     * @return void
     */
    function addRow($row, $append = true)
    {
        if ($append) {
            $this->_data[] = array_values($row);
        } else {
            array_unshift($this->_data, array_values($row));
        }

        $this->_updateRowsCols($row);
    }

    /**
     * Inserts a row after a given row number in the table.
     *
     * If $row_id is not given it will prepend the row.
     *
     * @param array   $row    The data to insert.
     * @param integer $row_id Row number to insert before.
     *
     * @return void
     */
    function insertRow($row, $row_id = 0)
    {
        array_splice($this->_data, $row_id, 0, array($row));

        $this->_updateRowsCols($row);
    }

    /**
     * Adds a column to the table.
     *
     * @param array   $col_data The data of the column.
     * @param integer $col_id   The column index to populate.
     * @param integer $row_id   If starting row is not zero, specify it here.
     *
     * @return void
     */
    function addCol($col_data, $col_id = 0, $row_id = 0)
    {
        foreach ($col_data as $col_cell) {
            $this->_data[$row_id++][$col_id] = $col_cell;
        }

        $this->_updateRowsCols();
        $this->_max_cols = max($this->_max_cols, $col_id + 1);
    }

    /**
     * Adds data to the table.
     *
     * @param array   $data   A two dimensional array with the table data.
     * @param integer $col_id Starting column number.
     * @param integer $row_id Starting row number.
     *
     * @return void
     */
    function addData($data, $col_id = 0, $row_id = 0)
    {
        foreach ($data as $row) {
            if ($row === CONSOLE_TABLE_HORIZONTAL_RULE) {
                $this->_data[$row_id] = CONSOLE_TABLE_HORIZONTAL_RULE;
                $row_id++;
                continue;
            }
            $starting_col = $col_id;
            foreach ($row as $cell) {
                $this->_data[$row_id][$starting_col++] = $cell;
            }
            $this->_updateRowsCols();
            $this->_max_cols = max($this->_max_cols, $starting_col);
            $row_id++;
        }
    }

    /**
     * Adds a horizontal seperator to the table.
     *
     * @return void
     */
    function addSeparator()
    {
        $this->_data[] = CONSOLE_TABLE_HORIZONTAL_RULE;
    }

    /**
     * Returns the generated table.
     *
     * @return string  The generated table.
     */
    function getTable()
    {
        $this->_applyFilters();
        $this->_calculateTotals();
        $this->_validateTable();

        return $this->_buildTable();
    }

    /**
     * Calculates totals for columns.
     *
     * @return void
     */
    function _calculateTotals()
    {
        if (empty($this->_calculateTotals)) {
            return;
        }

        $this->addSeparator();

        $totals = array();
        foreach ($this->_data as $row) {
            if (is_array($row)) {
                foreach ($this->_calculateTotals as $columnID) {
                    $totals[$columnID] += $row[$columnID];
                }
            }
        }

        $this->_data[] = $totals;
        $this->_updateRowsCols();
    }

    /**
     * Applies any column filters to the data.
     *
     * @return void
     */
    function _applyFilters()
    {
        if (empty($this->_filters)) {
            return;
        }

        foreach ($this->_filters as $filter) {
            $column   = $filter[0];
            $callback = $filter[1];

            foreach ($this->_data as $row_id => $row_data) {
                if ($row_data !== CONSOLE_TABLE_HORIZONTAL_RULE) {
                    $this->_data[$row_id][$column] =
                        call_user_func($callback, $row_data[$column]);
                }
            }
        }
    }

    /**
     * Ensures that column and row counts are correct.
     *
     * @return void
     */
    function _validateTable()
    {
        if (!empty($this->_headers)) {
            $this->_calculateRowHeight(-1, $this->_headers[0]);
        }

        for ($i = 0; $i < $this->_max_rows; $i++) {
            for ($j = 0; $j < $this->_max_cols; $j++) {
                if (!isset($this->_data[$i][$j]) &&
                    (!isset($this->_data[$i]) ||
                     $this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE)) {
                    $this->_data[$i][$j] = '';
                }

            }
            $this->_calculateRowHeight($i, $this->_data[$i]);

            if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE) {
                 ksort($this->_data[$i]);
            }

        }

        $this->_splitMultilineRows();

        // Update cell lengths.
        for ($i = 0; $i < count($this->_headers); $i++) {
            $this->_calculateCellLengths($this->_headers[$i]);
        }
        for ($i = 0; $i < $this->_max_rows; $i++) {
            $this->_calculateCellLengths($this->_data[$i]);
        }

        ksort($this->_data);
    }

    /**
     * Splits multiline rows into many smaller one-line rows.
     *
     * @return void
     */
    function _splitMultilineRows()
    {
        ksort($this->_data);
        $sections          = array(&$this->_headers, &$this->_data);
        $max_rows          = array(count($this->_headers), $this->_max_rows);
        $row_height_offset = array(-1, 0);

        for ($s = 0; $s <= 1; $s++) {
            $inserted = 0;
            $new_data = $sections[$s];

            for ($i = 0; $i < $max_rows[$s]; $i++) {
                // Process only rows that have many lines.
                $height = $this->_row_heights[$i + $row_height_offset[$s]];
                if ($height > 1) {
                    // Split column data into one-liners.
                    $split = array();
                    for ($j = 0; $j < $this->_max_cols; $j++) {
                        $split[$j] = preg_split('/\r?\n|\r/',
                                                $sections[$s][$i][$j]);
                    }

                    $new_rows = array();
                    // Construct new 'virtual' rows - insert empty strings for
                    // columns that have less lines that the highest one.
                    for ($i2 = 0; $i2 < $height; $i2++) {
                        for ($j = 0; $j < $this->_max_cols; $j++) {
                            $new_rows[$i2][$j] = !isset($split[$j][$i2])
                                ? ''
                                : $split[$j][$i2];
                        }
                    }

                    // Replace current row with smaller rows.  $inserted is
                    // used to take account of bigger array because of already
                    // inserted rows.
                    array_splice($new_data, $i + $inserted, 1, $new_rows);
                    $inserted += count($new_rows) - 1;
                }
            }

            // Has the data been modified?
            if ($inserted > 0) {
                $sections[$s] = $new_data;
                $this->_updateRowsCols();
            }
        }
    }

    /**
     * Builds the table.
     *
     * @return string  The generated table string.
     */
    function _buildTable()
    {
        if (!count($this->_data)) {
            return '';
        }

        $vertical = $this->_border['vertical'];
        $separator = $this->_getSeparator();

        $return = array();
        for ($i = 0; $i < count($this->_data); $i++) {
            for ($j = 0; $j < count($this->_data[$i]); $j++) {
                if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE &&
                    $this->_strlen($this->_data[$i][$j]) <
                    $this->_cell_lengths[$j]) {
                    $this->_data[$i][$j] = $this->_strpad($this->_data[$i][$j],
                                                          $this->_cell_lengths[$j],
                                                          ' ',
                                                          $this->_col_align[$j]);
                }
            }

            if ($this->_data[$i] !== CONSOLE_TABLE_HORIZONTAL_RULE) {
                $row_begin = $this->_borderVisibility['left']
                    ? $vertical . str_repeat(' ', $this->_padding)
                    : '';
                $row_end = $this->_borderVisibility['right']
                    ? str_repeat(' ', $this->_padding) . $vertical
                    : '';
                $implode_char = str_repeat(' ', $this->_padding) . $vertical
                    . str_repeat(' ', $this->_padding);
                $return[]     = $row_begin
                    . implode($implode_char, $this->_data[$i]) . $row_end;
            } elseif (!empty($separator)) {
                $return[] = $separator;
            }

        }

        $return = implode(PHP_EOL, $return);
        if (!empty($separator)) {
            if ($this->_borderVisibility['inner']) {
                $return = $separator . PHP_EOL . $return;
            }
            if ($this->_borderVisibility['bottom']) {
                $return .= PHP_EOL . $separator;
            }
        }
        $return .= PHP_EOL;

        if (!empty($this->_headers)) {
            $return = $this->_getHeaderLine() .  PHP_EOL . $return;
        }

        return $return;
    }

    /**
     * Creates a horizontal separator for header separation and table
     * start/end etc.
     *
     * @return string  The horizontal separator.
     */
    function _getSeparator()
    {
        if (!$this->_border) {
            return;
        }

        $horizontal = $this->_border['horizontal'];
        $intersection = $this->_border['intersection'];

        $return = array();
        foreach ($this->_cell_lengths as $cl) {
            $return[] = str_repeat($horizontal, $cl);
        }

        $row_begin = $this->_borderVisibility['left']
            ? $intersection . str_repeat($horizontal, $this->_padding)
            : '';
        $row_end = $this->_borderVisibility['right']
            ? str_repeat($horizontal, $this->_padding) . $intersection
            : '';
        $implode_char = str_repeat($horizontal, $this->_padding) . $intersection
            . str_repeat($horizontal, $this->_padding);

        return $row_begin . implode($implode_char, $return) . $row_end;
    }

    /**
     * Returns the header line for the table.
     *
     * @return string  The header line of the table.
     */
    function _getHeaderLine()
    {
        // Make sure column count is correct
        for ($j = 0; $j < count($this->_headers); $j++) {
            for ($i = 0; $i < $this->_max_cols; $i++) {
                if (!isset($this->_headers[$j][$i])) {
                    $this->_headers[$j][$i] = '';
                }
            }
        }

        for ($j = 0; $j < count($this->_headers); $j++) {
            for ($i = 0; $i < count($this->_headers[$j]); $i++) {
                if ($this->_strlen($this->_headers[$j][$i]) <
                    $this->_cell_lengths[$i]) {
                    $this->_headers[$j][$i] =
                        $this->_strpad($this->_headers[$j][$i],
                                       $this->_cell_lengths[$i],
                                       ' ',
                                       $this->_col_align[$i]);
                }
            }
        }

        $vertical = $this->_border['vertical'];
        $row_begin = $this->_borderVisibility['left']
            ? $vertical . str_repeat(' ', $this->_padding)
            : '';
        $row_end = $this->_borderVisibility['right']
            ? str_repeat(' ', $this->_padding) . $vertical
            : '';
        $implode_char = str_repeat(' ', $this->_padding) . $vertical
            . str_repeat(' ', $this->_padding);

        $separator = $this->_getSeparator();
        if (!empty($separator) && $this->_borderVisibility['top']) {
            $return[] = $separator;
        }
        for ($j = 0; $j < count($this->_headers); $j++) {
            $return[] = $row_begin
                . implode($implode_char, $this->_headers[$j]) . $row_end;
        }

        return implode(PHP_EOL, $return);
    }

    /**
     * Updates values for maximum columns and rows.
     *
     * @param array $rowdata Data array of a single row.
     *
     * @return void
     */
    function _updateRowsCols($rowdata = null)
    {
        // Update maximum columns.
        $this->_max_cols = max($this->_max_cols, count($rowdata));

        // Update maximum rows.
        ksort($this->_data);
        $keys            = array_keys($this->_data);
        $this->_max_rows = end($keys) + 1;

        switch ($this->_defaultAlign) {
        case CONSOLE_TABLE_ALIGN_CENTER:
            $pad = STR_PAD_BOTH;
            break;
        case CONSOLE_TABLE_ALIGN_RIGHT:
            $pad = STR_PAD_LEFT;
            break;
        default:
            $pad = STR_PAD_RIGHT;
            break;
        }

        // Set default column alignments
        for ($i = 0; $i < $this->_max_cols; $i++) {
            if (!isset($this->_col_align[$i])) {
                $this->_col_align[$i] = $pad;
            }
        }
    }

    /**
     * Calculates the maximum length for each column of a row.
     *
     * @param array $row The row data.
     *
     * @return void
     */
    function _calculateCellLengths($row)
    {
        for ($i = 0; $i < count($row); $i++) {
            if (!isset($this->_cell_lengths[$i])) {
                $this->_cell_lengths[$i] = 0;
            }
            $this->_cell_lengths[$i] = max($this->_cell_lengths[$i],
                                           $this->_strlen($row[$i]));
        }
    }

    /**
     * Calculates the maximum height for all columns of a row.
     *
     * @param integer $row_number The row number.
     * @param array   $row        The row data.
     *
     * @return void
     */
    function _calculateRowHeight($row_number, $row)
    {
        if (!isset($this->_row_heights[$row_number])) {
            $this->_row_heights[$row_number] = 1;
        }

        // Do not process horizontal rule rows.
        if ($row === CONSOLE_TABLE_HORIZONTAL_RULE) {
            return;
        }

        for ($i = 0, $c = count($row); $i < $c; ++$i) {
            $lines                           = preg_split('/\r?\n|\r/', $row[$i]);
            $this->_row_heights[$row_number] = max($this->_row_heights[$row_number],
                                                   count($lines));
        }
    }

    /**
     * Returns the character length of a string.
     *
     * @param string $str A multibyte or singlebyte string.
     *
     * @return integer  The string length.
     */
    function _strlen($str)
    {
        static $mbstring;

        // Strip ANSI color codes if requested.
        if ($this->_ansiColor) {
            $str = $this->_ansiColor->strip($str);
        }

        // Cache expensive function_exists() calls.
        if (!isset($mbstring)) {
            $mbstring = function_exists('mb_strwidth');
        }

        if ($mbstring) {
            return mb_strwidth($str, $this->_charset);
        }

        return strlen($str);
    }

    /**
     * Returns part of a string.
     *
     * @param string  $string The string to be converted.
     * @param integer $start  The part's start position, zero based.
     * @param integer $length The part's length.
     *
     * @return string  The string's part.
     */
    function _substr($string, $start, $length = null)
    {
        static $mbstring;

        // Cache expensive function_exists() calls.
        if (!isset($mbstring)) {
            $mbstring = function_exists('mb_substr');
        }

        if (is_null($length)) {
            $length = $this->_strlen($string);
        }
        if ($mbstring) {
            $ret = @mb_substr($string, $start, $length, $this->_charset);
            if (!empty($ret)) {
                return $ret;
            }
        }
        return substr($string, $start, $length);
    }

    /**
     * Returns a string padded to a certain length with another string.
     *
     * This method behaves exactly like str_pad but is multibyte safe.
     *
     * @param string  $input  The string to be padded.
     * @param integer $length The length of the resulting string.
     * @param string  $pad    The string to pad the input string with. Must
     *                        be in the same charset like the input string.
     * @param const   $type   The padding type. One of STR_PAD_LEFT,
     *                        STR_PAD_RIGHT, or STR_PAD_BOTH.
     *
     * @return string  The padded string.
     */
    function _strpad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT)
    {
        $mb_length  = $this->_strlen($input);
        $sb_length  = strlen($input);
        $pad_length = $this->_strlen($pad);

        /* Return if we already have the length. */
        if ($mb_length >= $length) {
            return $input;
        }

        /* Shortcut for single byte strings. */
        if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
            return str_pad($input, $length, $pad, $type);
        }

        switch ($type) {
        case STR_PAD_LEFT:
            $left   = $length - $mb_length;
            $output = $this->_substr(str_repeat($pad, ceil($left / $pad_length)),
                                     0, $left, $this->_charset) . $input;
            break;
        case STR_PAD_BOTH:
            $left   = floor(($length - $mb_length) / 2);
            $right  = ceil(($length - $mb_length) / 2);
            $output = $this->_substr(str_repeat($pad, ceil($left / $pad_length)),
                                     0, $left, $this->_charset) .
                $input .
                $this->_substr(str_repeat($pad, ceil($right / $pad_length)),
                               0, $right, $this->_charset);
            break;
        case STR_PAD_RIGHT:
            $right  = $length - $mb_length;
            $output = $input .
                $this->_substr(str_repeat($pad, ceil($right / $pad_length)),
                               0, $right, $this->_charset);
            break;
        }

        return $output;
    }

}

 * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com)
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
 * @link      http://phpdoc.org
 */

namespace phpDocumentor\Reflection\DocBlock;

/**
 * The context in which a DocBlock occurs.
 *
 * @author  Vasil Rangelov 
 * @license http://www.opensource.org/licenses/mit-license.php MIT
 * @link    http://phpdoc.org
 */
class Context
{
    /** @var string The current namespace. */
    protected $namespace = '';

    /** @var array List of namespace aliases => Fully Qualified Namespace. */
    protected $namespace_aliases = array();
    
    /** @var string Name of the structural element, within the namespace. */
    protected $lsen = '';
    
    /**
     * Cteates a new context.
     * @param string $namespace         The namespace where this DocBlock
     *     resides in.
     * @param array  $namespace_aliases List of namespace aliases => Fully
     *     Qualified Namespace.
     * @param string $lsen              Name of the structural element, within
     *     the namespace.
     */
    public function __construct(
        $namespace = '',
        array $namespace_aliases = array(),
        $lsen = ''
    ) {
        if (!empty($namespace)) {
            $this->setNamespace($namespace);
        }
        $this->setNamespaceAliases($namespace_aliases);
        $this->setLSEN($lsen);
    }

    /**
     * @return string The namespace where this DocBlock resides in.
     */
    public function getNamespace()
    {
        return $this->namespace;
    }

    /**
     * @return array List of namespace aliases => Fully Qualified Namespace.
     */
    public function getNamespaceAliases()
    {
        return $this->namespace_aliases;
    }
    
    /**
     * Returns the Local Structural Element Name.
     * 
     * @return string Name of the structural element, within the namespace.
     */
    public function getLSEN()
    {
        return $this->lsen;
    }
    
    /**
     * Sets a new namespace.
     * 
     * Sets a new namespace for the context. Leading and trailing slashes are
     * trimmed, and the keywords "global" and "default" are treated as aliases
     * to no namespace.
     * 
     * @param string $namespace The new namespace to set.
     * 
     * @return $this
     */
    public function setNamespace($namespace)
    {
        if ('global' !== $namespace
            && 'default' !== $namespace
        ) {
            // Srip leading and trailing slash
            $this->namespace = trim((string)$namespace, '\\');
        } else {
            $this->namespace = '';
        }
        return $this;
    }
    
    /**
     * Sets the namespace aliases, replacing all previous ones.
     * 
     * @param array $namespace_aliases List of namespace aliases => Fully
     *     Qualified Namespace.
     * 
     * @return $this
     */
    public function setNamespaceAliases(array $namespace_aliases)
    {
        $this->namespace_aliases = array();
        foreach ($namespace_aliases as $alias => $fqnn) {
            $this->setNamespaceAlias($alias, $fqnn);
        }
        return $this;
    }
    
    /**
     * Adds a namespace alias to the context.
     * 
     * @param string $alias The alias name (the part after "as", or the last
     *     part of the Fully Qualified Namespace Name) to add.
     * @param string $fqnn  The Fully Qualified Namespace Name for this alias.
     *     Any form of leading/trailing slashes are accepted, but what will be
     *     stored is a name, prefixed with a slash, and no trailing slash.
     * 
     * @return $this
     */
    public function setNamespaceAlias($alias, $fqnn)
    {
        $this->namespace_aliases[$alias] = '\\' . trim((string)$fqnn, '\\');
        return $this;
    }
    
    /**
     * Sets a new Local Structural Element Name.
     * 
     * Sets a new Local Structural Element Name. A local name also contains
     * punctuation determining the kind of structural element (e.g. trailing "("
     * and ")" for functions and methods).
     * 
     * @param string $lsen The new local name of a structural element.
     * 
     * @return $this
     */
    public function setLSEN($lsen)
    {
        $this->lsen = (string)$lsen;
        return $this;
    }
}

 * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com)
 * @license   http://www.opensource.org/licenses/mit-license.php MIT
 * @link      http://phpdoc.org
 */

namespace phpDocumentor\Reflection\DocBlock;

use phpDocumentor\Reflection\DocBlock;

/**
 * Parses a Description of a DocBlock or tag.
 *
 * @author  Mike van Riel 
 * @license http://www.opensource.org/licenses/mit-license.php MIT
 * @link    http://phpdoc.org
 */
class Description implements \Reflector
{
    /** @var string */
    protected $contents = '';

    /** @var array The contents, as an array of strings and Tag objects. */
    protected $parsedContents = null;

    /** @var DocBlock The DocBlock which this description belongs to. */
    protected $docblock = null;

    /**
     * Populates the fields of a description.
     *
     * @param string   $content  The description's conetnts.
     * @param DocBlock $docblock The DocBlock which this description belongs to.
     */
    public function __construct($content, DocBlock $docblock = null)
    {
        $this->setContent($content)->setDocBlock($docblock);
    }

    /**
     * Gets the text of this description.
     *
     * @return string
     */
    public function getContents()
    {
        return $this->contents;
    }

    /**
     * Sets the text of this description.
     *
     * @param string $content The new text of this description.
     *
     * @return $this
     */
    public function setContent($content)
    {
        $this->contents = trim($content);

        $this->parsedContents = null;
        return $this;
    }

    /**
     * Returns the parsed text of this description.
     *
     * @return array An array of strings and tag objects, in the order they
     *     occur within the description.
     */
    public function getParsedContents()
    {
        if (null === $this->parsedContents) {
            $this->parsedContents = preg_split(
                '/\{
                    # "{@}" is not a valid inline tag. This ensures that
                    # we do not treat it as one, but treat it literally.
                    (?!@\})
                    # We want to capture the whole tag line, but without the
                    # inline tag delimiters.
                    (\@
                        # Match everything up to the next delimiter.
                        [^{}]*
                        # Nested inline tag content should not be captured, or
                        # it will appear in the result separately.
                        (?:
                            # Match nested inline tags.
                            (?:
                                # Because we did not catch the tag delimiters
                                # earlier, we must be explicit with them here.
                                # Notice that this also matches "{}", as a way
                                # to later introduce it as an escape sequence.
                                \{(?1)?\}
                                |
                                # Make sure we match hanging "{".
                                \{
                            )
                            # Match content after the nested inline tag.
                            [^{}]*
                        )* # If there are more inline tags, match them as well.
                           # We use "*" since there may not be any nested inline
                           # tags.
                    )
                \}/Sux',
                $this->contents,
                null,
                PREG_SPLIT_DELIM_CAPTURE
            );

            $count = count($this->parsedContents);
            for ($i=1; $i<$count; $i += 2) {
                $this->parsedContents[$i] = Tag::createInstance(
                    $this->parsedContents[$i],
                    $this->docblock
                );
            }

            //In order to allow "literal" inline tags, the otherwise invalid
            //sequence "{@}" is changed to "@", and "{}" is changed to "}".
            //See unit tests for examples.
            for ($i=0; $i<$count; $i += 2) {
                $this->parsedContents[$i] = str_replace(
                    array('{@}', '{}'),
                    array('@', '}'),
                    $this->parsedContents[$i]
                );
            }
        }
        return $this->parsedContents;
    }

    /**
     * Return a formatted variant of the Long Description using MarkDown.
     *
     * @todo this should become a more intelligent piece of code where the
     *     configuration contains a setting what format long descriptions are.
     *
     * @codeCoverageIgnore Will be removed soon, in favor of adapters at
     *     PhpDocumentor itself that will process text in various formats.
     *
     * @return string
     */
    public function getFormattedContents()
    {
        $result = $this->contents;

        // if the long description contains a plain HTML  element, surround
        // it with a pre element. Please note that we explicitly used str_replace
        // and not preg_replace to gain performance
        if (strpos($result, '') !== false) {
            $result = str_replace(
                array('', "\r\n", "\n", "\r", ''),
                array('
', '', '', '', '
'), $result ); } if (class_exists('Parsedown')) { $markdown = \Parsedown::instance(); $result = $markdown->parse($result); } elseif (class_exists('dflydev\markdown\MarkdownExtraParser')) { $markdown = new \dflydev\markdown\MarkdownExtraParser(); $result = $markdown->transformMarkdown($result); } return trim($result); } /** * Gets the docblock this tag belongs to. * * @return DocBlock The docblock this description belongs to. */ public function getDocBlock() { return $this->docblock; } /** * Sets the docblock this tag belongs to. * * @param DocBlock $docblock The new docblock this description belongs to. * Setting NULL removes any association. * * @return $this */ public function setDocBlock(DocBlock $docblock = null) { $this->docblock = $docblock; return $this; } /** * Builds a string representation of this object. * * @todo determine the exact format as used by PHP Reflection * and implement it. * * @return void * @codeCoverageIgnore Not yet implemented */ public static function export() { throw new \Exception('Not yet implemented'); } /** * Returns the long description as a string. * * @return string */ public function __toString() { return $this->getContents(); } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock; /** * The location a DocBlock occurs within a file. * * @author Vasil Rangelov * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class Location { /** @var int Line where the DocBlock text starts. */ protected $lineNumber = 0; /** @var int Column where the DocBlock text starts. */ protected $columnNumber = 0; public function __construct( $lineNumber = 0, $columnNumber = 0 ) { $this->setLineNumber($lineNumber)->setColumnNumber($columnNumber); } /** * @return int Line where the DocBlock text starts. */ public function getLineNumber() { return $this->lineNumber; } /** * * @param type $lineNumber * @return $this */ public function setLineNumber($lineNumber) { $this->lineNumber = (int)$lineNumber; return $this; } /** * @return int Column where the DocBlock text starts. */ public function getColumnNumber() { return $this->columnNumber; } /** * * @param int $columnNumber * @return $this */ public function setColumnNumber($columnNumber) { $this->columnNumber = (int)$columnNumber; return $this; } } * @copyright 2013 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock; /** * Serializes a DocBlock instance. * * @author Barry vd. Heuvel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class Serializer { /** @var string The string to indent the comment with. */ protected $indentString = ' '; /** @var int The number of times the indent string is repeated. */ protected $indent = 0; /** @var bool Whether to indent the first line. */ protected $isFirstLineIndented = true; /** @var int|null The max length of a line. */ protected $lineLength = null; /** * Create a Serializer instance. * * @param int $indent The number of times the indent string is * repeated. * @param string $indentString The string to indent the comment with. * @param bool $indentFirstLine Whether to indent the first line. * @param int|null $lineLength The max length of a line or NULL to * disable line wrapping. */ public function __construct( $indent = 0, $indentString = ' ', $indentFirstLine = true, $lineLength = null ) { $this->setIndentationString($indentString); $this->setIndent($indent); $this->setIsFirstLineIndented($indentFirstLine); $this->setLineLength($lineLength); } /** * Sets the string to indent comments with. * * @param string $indentationString The string to indent comments with. * * @return $this This serializer object. */ public function setIndentationString($indentString) { $this->indentString = (string)$indentString; return $this; } /** * Gets the string to indent comments with. * * @return string The indent string. */ public function getIndentationString() { return $this->indentString; } /** * Sets the number of indents. * * @param int $indent The number of times the indent string is repeated. * * @return $this This serializer object. */ public function setIndent($indent) { $this->indent = (int)$indent; return $this; } /** * Gets the number of indents. * * @return int The number of times the indent string is repeated. */ public function getIndent() { return $this->indent; } /** * Sets whether or not the first line should be indented. * * Sets whether or not the first line (the one with the "/**") should be * indented. * * @param bool $indentFirstLine The new value for this setting. * * @return $this This serializer object. */ public function setIsFirstLineIndented($indentFirstLine) { $this->isFirstLineIndented = (bool)$indentFirstLine; return $this; } /** * Gets whether or not the first line should be indented. * * @return bool Whether or not the first line should be indented. */ public function isFirstLineIndented() { return $this->isFirstLineIndented; } /** * Sets the line length. * * Sets the length of each line in the serialization. Content will be * wrapped within this limit. * * @param int|null $lineLength The length of each line. NULL to disable line * wrapping altogether. * * @return $this This serializer object. */ public function setLineLength($lineLength) { $this->lineLength = null === $lineLength ? null : (int)$lineLength; return $this; } /** * Gets the line length. * * @return int|null The length of each line or NULL if line wrapping is * disabled. */ public function getLineLength() { return $this->lineLength; } /** * Generate a DocBlock comment. * * @param DocBlock The DocBlock to serialize. * * @return string The serialized doc block. */ public function getDocComment(DocBlock $docblock) { $indent = str_repeat($this->indentString, $this->indent); $firstIndent = $this->isFirstLineIndented ? $indent : ''; $text = $docblock->getText(); if ($this->lineLength) { //3 === strlen(' * ') $wrapLength = $this->lineLength - strlen($indent) - 3; $text = wordwrap($text, $wrapLength); } $text = str_replace("\n", "\n{$indent} * ", $text); $comment = "{$firstIndent}/**\n{$indent} * {$text}\n{$indent} *\n"; /** @var Tag $tag */ foreach ($docblock->getTags() as $tag) { $tagText = (string) $tag; if ($this->lineLength) { $tagText = wordwrap($tagText, $wrapLength); } $tagText = str_replace("\n", "\n{$indent} * ", $tagText); $comment .= "{$indent} * {$tagText}\n"; } $comment .= $indent . ' */'; return $comment; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for an @author tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class AuthorTag extends Tag { /** * PCRE regular expression matching any valid value for the name component. */ const REGEX_AUTHOR_NAME = '[^\<]*'; /** * PCRE regular expression matching any valid value for the email component. */ const REGEX_AUTHOR_EMAIL = '[^\>]*'; /** @var string The name of the author */ protected $authorName = ''; /** @var string The email of the author */ protected $authorEmail = ''; public function getContent() { if (null === $this->content) { $this->content = $this->authorName; if ('' != $this->authorEmail) { $this->content .= "<{$this->authorEmail}>"; } } return $this->content; } /** * {@inheritdoc} */ public function setContent($content) { parent::setContent($content); if (preg_match( '/^(' . self::REGEX_AUTHOR_NAME . ')(\<(' . self::REGEX_AUTHOR_EMAIL . ')\>)?$/u', $this->description, $matches )) { $this->authorName = trim($matches[1]); if (isset($matches[3])) { $this->authorEmail = trim($matches[3]); } } return $this; } /** * Gets the author's name. * * @return string The author's name. */ public function getAuthorName() { return $this->authorName; } /** * Sets the author's name. * * @param string $authorName The new author name. * An invalid value will set an empty string. * * @return $this */ public function setAuthorName($authorName) { $this->content = null; $this->authorName = preg_match('/^' . self::REGEX_AUTHOR_NAME . '$/u', $authorName) ? $authorName : ''; return $this; } /** * Gets the author's email. * * @return string The author's email. */ public function getAuthorEmail() { return $this->authorEmail; } /** * Sets the author's email. * * @param string $authorEmail The new author email. * An invalid value will set an empty string. * * @return $this */ public function setAuthorEmail($authorEmail) { $this->authorEmail = preg_match('/^' . self::REGEX_AUTHOR_EMAIL . '$/u', $authorEmail) ? $authorEmail : ''; $this->content = null; return $this; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @covers tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class CoversTag extends SeeTag { } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag\VersionTag; /** * Reflection class for a @deprecated tag in a Docblock. * * @author Vasil Rangelov * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class DeprecatedTag extends VersionTag { } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @example tag in a Docblock. * * @author Vasil Rangelov * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class ExampleTag extends SourceTag { /** * @var string Path to a file to use as an example. * May also be an absolute URI. */ protected $filePath = ''; /** * @var bool Whether the file path component represents an URI. * This determines how the file portion appears at {@link getContent()}. */ protected $isURI = false; /** * {@inheritdoc} */ public function getContent() { if (null === $this->content) { $filePath = ''; if ($this->isURI) { if (false === strpos($this->filePath, ':')) { $filePath = str_replace( '%2F', '/', rawurlencode($this->filePath) ); } else { $filePath = $this->filePath; } } else { $filePath = '"' . $this->filePath . '"'; } $this->content = $filePath . ' ' . parent::getContent(); } return $this->content; } /** * {@inheritdoc} */ public function setContent($content) { Tag::setContent($content); if (preg_match( '/^ # File component (?: # File path in quotes \"([^\"]+)\" | # File URI (\S+) ) # Remaining content (parsed by SourceTag) (?:\s+(.*))? $/sux', $this->description, $matches )) { if ('' !== $matches[1]) { $this->setFilePath($matches[1]); } else { $this->setFileURI($matches[2]); } if (isset($matches[3])) { parent::setContent($matches[3]); } else { $this->setDescription(''); } $this->content = $content; } return $this; } /** * Returns the file path. * * @return string Path to a file to use as an example. * May also be an absolute URI. */ public function getFilePath() { return $this->filePath; } /** * Sets the file path. * * @param string $filePath The new file path to use for the example. * * @return $this */ public function setFilePath($filePath) { $this->isURI = false; $this->filePath = trim($filePath); $this->content = null; return $this; } /** * Sets the file path as an URI. * * This function is equivalent to {@link setFilePath()}, except that it * convers an URI to a file path before that. * * There is no getFileURI(), as {@link getFilePath()} is compatible. * * @param type $uri The new file URI to use as an example. */ public function setFileURI($uri) { $this->isURI = true; if (false === strpos($uri, ':')) { //Relative URL $this->filePath = rawurldecode( str_replace(array('/', '\\'), '%2F', $uri) ); } else { //Absolute URL or URI. $this->filePath = $uri; } $this->content = null; return $this; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @link tag in a Docblock. * * @author Ben Selby * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class LinkTag extends Tag { /** @var string */ protected $link = ''; /** * {@inheritdoc} */ public function getContent() { if (null === $this->content) { $this->content = "{$this->link} {$this->description}"; } return $this->content; } /** * {@inheritdoc} */ public function setContent($content) { parent::setContent($content); $parts = preg_split('/\s+/Su', $this->description, 2); $this->link = $parts[0]; $this->setDescription(isset($parts[1]) ? $parts[1] : $parts[0]); $this->content = $content; return $this; } /** * Gets the link * * @return string */ public function getLink() { return $this->link; } /** * Sets the link * * @param string $link The link * * @return $this */ public function setLink($link) { $this->link = $link; $this->content = null; return $this; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @method in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class MethodTag extends ReturnTag { /** @var string */ protected $method_name = ''; /** @var string */ protected $arguments = ''; /** @var bool */ protected $isStatic = false; /** * {@inheritdoc} */ public function getContent() { if (null === $this->content) { $this->content = ''; if ($this->isStatic) { $this->content .= 'static '; } $this->content .= $this->type . " {$this->method_name}({$this->arguments}) " . $this->description; } return $this->content; } /** * {@inheritdoc} */ public function setContent($content) { Tag::setContent($content); // 1. none or more whitespace // 2. optionally the keyword "static" followed by whitespace // 3. optionally a word with underscores followed by whitespace : as // type for the return value // 4. then optionally a word with underscores followed by () and // whitespace : as method name as used by phpDocumentor // 5. then a word with underscores, followed by ( and any character // until a ) and whitespace : as method name with signature // 6. any remaining text : as description if (preg_match( '/^ # Static keyword # Declates a static method ONLY if type is also present (?: (static) \s+ )? # Return type (?: ([\w\|_\\\\]+) \s+ )? # Legacy method name (not captured) (?: [\w_]+\(\)\s+ )? # Method name ([\w\|_\\\\]+) # Arguments \(([^\)]*)\) \s* # Description (.*) $/sux', $this->description, $matches )) { list( , $static, $this->type, $this->method_name, $this->arguments, $this->description ) = $matches; if ($static) { if (!$this->type) { $this->type = 'static'; } else { $this->isStatic = true; } } else { if (!$this->type) { $this->type = 'void'; } } $this->parsedDescription = null; } return $this; } /** * Sets the name of this method. * * @param string $method_name The name of the method. * * @return $this */ public function setMethodName($method_name) { $this->method_name = $method_name; $this->content = null; return $this; } /** * Retrieves the method name. * * @return string */ public function getMethodName() { return $this->method_name; } /** * Sets the arguments for this method. * * @param string $arguments A comma-separated arguments line. * * @return void */ public function setArguments($arguments) { $this->arguments = $arguments; $this->content = null; return $this; } /** * Returns an array containing each argument as array of type and name. * * Please note that the argument sub-array may only contain 1 element if no * type was specified. * * @return string[] */ public function getArguments() { if (empty($this->arguments)) { return array(); } $arguments = explode(',', $this->arguments); foreach ($arguments as $key => $value) { $arguments[$key] = explode(' ', trim($value)); } return $arguments; } /** * Checks whether the method tag describes a static method or not. * * @return bool TRUE if the method declaration is for a static method, FALSE * otherwise. */ public function isStatic() { return $this->isStatic; } /** * Sets a new value for whether the method is static or not. * * @param bool $isStatic The new value to set. * * @return $this */ public function setIsStatic($isStatic) { $this->isStatic = $isStatic; $this->content = null; return $this; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @param tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class ParamTag extends ReturnTag { /** @var string */ protected $variableName = ''; /** @var bool determines whether this is a variadic argument */ protected $isVariadic = false; /** * {@inheritdoc} */ public function getContent() { if (null === $this->content) { $this->content = "{$this->type} {$this->variableName} {$this->description}"; } return $this->content; } /** * {@inheritdoc} */ public function setContent($content) { Tag::setContent($content); $parts = preg_split( '/(\s+)/Su', $this->description, 3, PREG_SPLIT_DELIM_CAPTURE ); // if the first item that is encountered is not a variable; it is a type if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] !== '$') ) { $this->type = array_shift($parts); array_shift($parts); } // if the next item starts with a $ or ...$ it must be the variable name if (isset($parts[0]) && (strlen($parts[0]) > 0) && ($parts[0][0] == '$' || substr($parts[0], 0, 4) === '...$') ) { $this->variableName = array_shift($parts); array_shift($parts); if (substr($this->variableName, 0, 3) === '...') { $this->isVariadic = true; $this->variableName = substr($this->variableName, 3); } } $this->setDescription(implode('', $parts)); $this->content = $content; return $this; } /** * Returns the variable's name. * * @return string */ public function getVariableName() { return $this->variableName; } /** * Sets the variable's name. * * @param string $name The new name for this variable. * * @return $this */ public function setVariableName($name) { $this->variableName = $name; $this->content = null; return $this; } /** * Returns whether this tag is variadic. * * @return boolean */ public function isVariadic() { return $this->isVariadic; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @property-read tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class PropertyReadTag extends PropertyTag { } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @property tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class PropertyTag extends ParamTag { } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @property-write tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class PropertyWriteTag extends PropertyTag { } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Type\Collection; /** * Reflection class for a @return tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class ReturnTag extends Tag { /** @var string The raw type component. */ protected $type = ''; /** @var Collection The parsed type component. */ protected $types = null; /** * {@inheritdoc} */ public function getContent() { if (null === $this->content) { $this->content = "{$this->type} {$this->description}"; } return $this->content; } /** * {@inheritdoc} */ public function setContent($content) { parent::setContent($content); $parts = preg_split('/\s+/Su', $this->description, 2); // any output is considered a type $this->type = $parts[0]; $this->types = null; $this->setDescription(isset($parts[1]) ? $parts[1] : ''); $this->content = $content; return $this; } /** * Returns the unique types of the variable. * * @return string[] */ public function getTypes() { return $this->getTypesCollection()->getArrayCopy(); } /** * Returns the type section of the variable. * * @return string */ public function getType() { return (string) $this->getTypesCollection(); } /** * Returns the type collection. * * @return void */ protected function getTypesCollection() { if (null === $this->types) { $this->types = new Collection( array($this->type), $this->docblock ? $this->docblock->getContext() : null ); } return $this->types; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @see tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class SeeTag extends Tag { /** @var string */ protected $refers = null; /** * {@inheritdoc} */ public function getContent() { if (null === $this->content) { $this->content = "{$this->refers} {$this->description}"; } return $this->content; } /** * {@inheritdoc} */ public function setContent($content) { parent::setContent($content); $parts = preg_split('/\s+/Su', $this->description, 2); // any output is considered a type $this->refers = $parts[0]; $this->setDescription(isset($parts[1]) ? $parts[1] : ''); $this->content = $content; return $this; } /** * Gets the structural element this tag refers to. * * @return string */ public function getReference() { return $this->refers; } /** * Sets the structural element this tag refers to. * * @param string $refers The new type this tag refers to. * * @return $this */ public function setReference($refers) { $this->refers = $refers; $this->content = null; return $this; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag\VersionTag; /** * Reflection class for a @since tag in a Docblock. * * @author Vasil Rangelov * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class SinceTag extends VersionTag { } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @source tag in a Docblock. * * @author Vasil Rangelov * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class SourceTag extends Tag { /** * @var int The starting line, relative to the structural element's * location. */ protected $startingLine = 1; /** * @var int|null The number of lines, relative to the starting line. NULL * means "to the end". */ protected $lineCount = null; /** * {@inheritdoc} */ public function getContent() { if (null === $this->content) { $this->content = "{$this->startingLine} {$this->lineCount} {$this->description}"; } return $this->content; } /** * {@inheritdoc} */ public function setContent($content) { parent::setContent($content); if (preg_match( '/^ # Starting line ([1-9]\d*) \s* # Number of lines (?: ((?1)) \s+ )? # Description (.*) $/sux', $this->description, $matches )) { $this->startingLine = (int)$matches[1]; if (isset($matches[2]) && '' !== $matches[2]) { $this->lineCount = (int)$matches[2]; } $this->setDescription($matches[3]); $this->content = $content; } return $this; } /** * Gets the starting line. * * @return int The starting line, relative to the structural element's * location. */ public function getStartingLine() { return $this->startingLine; } /** * Sets the starting line. * * @param int $startingLine The new starting line, relative to the * structural element's location. * * @return $this */ public function setStartingLine($startingLine) { $this->startingLine = $startingLine; $this->content = null; return $this; } /** * Returns the number of lines. * * @return int|null The number of lines, relative to the starting line. NULL * means "to the end". */ public function getLineCount() { return $this->lineCount; } /** * Sets the number of lines. * * @param int|null $lineCount The new number of lines, relative to the * starting line. NULL means "to the end". * * @return $this */ public function setLineCount($lineCount) { $this->lineCount = $lineCount; $this->content = null; return $this; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @throws tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class ThrowsTag extends ReturnTag { } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @uses tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class UsesTag extends SeeTag { } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @var tag in a Docblock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class VarTag extends ParamTag { } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tag; /** * Reflection class for a @version tag in a Docblock. * * @author Vasil Rangelov * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class VersionTag extends Tag { /** * PCRE regular expression matching a version vector. * Assumes the "x" modifier. */ const REGEX_VECTOR = '(?: # Normal release vectors. \d\S* | # VCS version vectors. Per PHPCS, they are expected to # follow the form of the VCS name, followed by ":", followed # by the version vector itself. # By convention, popular VCSes like CVS, SVN and GIT use "$" # around the actual version vector. [^\s\:]+\:\s*\$[^\$]+\$ )'; /** @var string The version vector. */ protected $version = ''; public function getContent() { if (null === $this->content) { $this->content = "{$this->version} {$this->description}"; } return $this->content; } /** * {@inheritdoc} */ public function setContent($content) { parent::setContent($content); if (preg_match( '/^ # The version vector (' . self::REGEX_VECTOR . ') \s* # The description (.+)? $/sux', $this->description, $matches )) { $this->version = $matches[1]; $this->setDescription(isset($matches[2]) ? $matches[2] : ''); $this->content = $content; } return $this; } /** * Gets the version section of the tag. * * @return string The version section of the tag. */ public function getVersion() { return $this->version; } /** * Sets the version section of the tag. * * @param string $version The new version section of the tag. * An invalid value will set an empty string. * * @return $this */ public function setVersion($version) { $this->version = preg_match('/^' . self::REGEX_VECTOR . '$/ux', $version) ? $version : ''; $this->content = null; return $this; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock; /** * Parses a tag definition for a DocBlock. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class Tag implements \Reflector { /** * PCRE regular expression matching a tag name. */ const REGEX_TAGNAME = '[\w\-\_\\\\]+'; /** @var string Name of the tag */ protected $tag = ''; /** * @var string|null Content of the tag. * When set to NULL, it means it needs to be regenerated. */ protected $content = ''; /** @var string Description of the content of this tag */ protected $description = ''; /** * @var array|null The description, as an array of strings and Tag objects. * When set to NULL, it means it needs to be regenerated. */ protected $parsedDescription = null; /** @var Location Location of the tag. */ protected $location = null; /** @var DocBlock The DocBlock which this tag belongs to. */ protected $docblock = null; /** * @var array An array with a tag as a key, and an FQCN to a class that * handles it as an array value. The class is expected to inherit this * class. */ private static $tagHandlerMappings = array( 'author' => '\phpDocumentor\Reflection\DocBlock\Tag\AuthorTag', 'covers' => '\phpDocumentor\Reflection\DocBlock\Tag\CoversTag', 'deprecated' => '\phpDocumentor\Reflection\DocBlock\Tag\DeprecatedTag', 'example' => '\phpDocumentor\Reflection\DocBlock\Tag\ExampleTag', 'link' => '\phpDocumentor\Reflection\DocBlock\Tag\LinkTag', 'method' => '\phpDocumentor\Reflection\DocBlock\Tag\MethodTag', 'param' => '\phpDocumentor\Reflection\DocBlock\Tag\ParamTag', 'property-read' => '\phpDocumentor\Reflection\DocBlock\Tag\PropertyReadTag', 'property' => '\phpDocumentor\Reflection\DocBlock\Tag\PropertyTag', 'property-write' => '\phpDocumentor\Reflection\DocBlock\Tag\PropertyWriteTag', 'return' => '\phpDocumentor\Reflection\DocBlock\Tag\ReturnTag', 'see' => '\phpDocumentor\Reflection\DocBlock\Tag\SeeTag', 'since' => '\phpDocumentor\Reflection\DocBlock\Tag\SinceTag', 'source' => '\phpDocumentor\Reflection\DocBlock\Tag\SourceTag', 'throw' => '\phpDocumentor\Reflection\DocBlock\Tag\ThrowsTag', 'throws' => '\phpDocumentor\Reflection\DocBlock\Tag\ThrowsTag', 'uses' => '\phpDocumentor\Reflection\DocBlock\Tag\UsesTag', 'var' => '\phpDocumentor\Reflection\DocBlock\Tag\VarTag', 'version' => '\phpDocumentor\Reflection\DocBlock\Tag\VersionTag' ); /** * Factory method responsible for instantiating the correct sub type. * * @param string $tag_line The text for this tag, including description. * @param DocBlock $docblock The DocBlock which this tag belongs to. * @param Location $location Location of the tag. * * @throws \InvalidArgumentException if an invalid tag line was presented. * * @return static A new tag object. */ final public static function createInstance( $tag_line, DocBlock $docblock = null, Location $location = null ) { if (!preg_match( '/^@(' . self::REGEX_TAGNAME . ')(?:\s*([^\s].*)|$)?/us', $tag_line, $matches )) { throw new \InvalidArgumentException( 'Invalid tag_line detected: ' . $tag_line ); } $handler = __CLASS__; if (isset(self::$tagHandlerMappings[$matches[1]])) { $handler = self::$tagHandlerMappings[$matches[1]]; } elseif (isset($docblock)) { $tagName = (string)new Type\Collection( array($matches[1]), $docblock->getContext() ); if (isset(self::$tagHandlerMappings[$tagName])) { $handler = self::$tagHandlerMappings[$tagName]; } } return new $handler( $matches[1], isset($matches[2]) ? $matches[2] : '', $docblock, $location ); } /** * Registers a handler for tags. * * Registers a handler for tags. The class specified is autoloaded if it's * not available. It must inherit from this class. * * @param string $tag Name of tag to regiser a handler for. When * registering a namespaced tag, the full name, along with a prefixing * slash MUST be provided. * @param string|null $handler FQCN of handler. Specifing NULL removes the * handler for the specified tag, if any. * * @return bool TRUE on success, FALSE on failure. */ final public static function registerTagHandler($tag, $handler) { $tag = trim((string)$tag); if (null === $handler) { unset(self::$tagHandlerMappings[$tag]); return true; } if ('' !== $tag && class_exists($handler, true) && is_subclass_of($handler, __CLASS__) && !strpos($tag, '\\') //Accept no slash, and 1st slash at offset 0. ) { self::$tagHandlerMappings[$tag] = $handler; return true; } return false; } /** * Parses a tag and populates the member variables. * * @param string $name Name of the tag. * @param string $content The contents of the given tag. * @param DocBlock $docblock The DocBlock which this tag belongs to. * @param Location $location Location of the tag. */ public function __construct( $name, $content, DocBlock $docblock = null, Location $location = null ) { $this ->setName($name) ->setContent($content) ->setDocBlock($docblock) ->setLocation($location); } /** * Gets the name of this tag. * * @return string The name of this tag. */ public function getName() { return $this->tag; } /** * Sets the name of this tag. * * @param string $name The new name of this tag. * * @return $this * @throws \InvalidArgumentException When an invalid tag name is provided. */ public function setName($name) { if (!preg_match('/^' . self::REGEX_TAGNAME . '$/u', $name)) { throw new \InvalidArgumentException( 'Invalid tag name supplied: ' . $name ); } $this->tag = $name; return $this; } /** * Gets the content of this tag. * * @return string */ public function getContent() { if (null === $this->content) { $this->content = $this->description; } return $this->content; } /** * Sets the content of this tag. * * @param string $content The new content of this tag. * * @return $this */ public function setContent($content) { $this->setDescription($content); $this->content = $content; return $this; } /** * Gets the description component of this tag. * * @return string */ public function getDescription() { return $this->description; } /** * Sets the description component of this tag. * * @param string $description The new description component of this tag. * * @return $this */ public function setDescription($description) { $this->content = null; $this->parsedDescription = null; $this->description = trim($description); return $this; } /** * Gets the parsed text of this description. * * @return array An array of strings and tag objects, in the order they * occur within the description. */ public function getParsedDescription() { if (null === $this->parsedDescription) { $description = new Description($this->description, $this->docblock); $this->parsedDescription = $description->getParsedContents(); } return $this->parsedDescription; } /** * Gets the docblock this tag belongs to. * * @return DocBlock The docblock this tag belongs to. */ public function getDocBlock() { return $this->docblock; } /** * Sets the docblock this tag belongs to. * * @param DocBlock $docblock The new docblock this tag belongs to. Setting * NULL removes any association. * * @return $this */ public function setDocBlock(DocBlock $docblock = null) { $this->docblock = $docblock; return $this; } /** * Gets the location of the tag. * * @return Location The tag's location. */ public function getLocation() { return $this->location; } /** * Sets the location of the tag. * * @param Location $location The new location of the tag. * * @return $this */ public function setLocation(Location $location = null) { $this->location = $location; return $this; } /** * Builds a string representation of this object. * * @todo determine the exact format as used by PHP Reflection and implement it. * * @return void * @codeCoverageIgnore Not yet implemented */ public static function export() { throw new \Exception('Not yet implemented'); } /** * Returns the tag as a serialized string * * @return string */ public function __toString() { return "@{$this->getName()} {$this->getContent()}"; } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection\DocBlock\Type; use phpDocumentor\Reflection\DocBlock\Context; /** * Collection * * @author Mike van Riel * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class Collection extends \ArrayObject { /** @var string Definition of the OR operator for types */ const OPERATOR_OR = '|'; /** @var string Definition of the ARRAY operator for types */ const OPERATOR_ARRAY = '[]'; /** @var string Definition of the NAMESPACE operator in PHP */ const OPERATOR_NAMESPACE = '\\'; /** @var string[] List of recognized keywords */ protected static $keywords = array( 'string', 'int', 'integer', 'bool', 'boolean', 'float', 'double', 'object', 'mixed', 'array', 'resource', 'void', 'null', 'scalar', 'callback', 'callable', 'false', 'true', 'self', '$this', 'static' ); /** * Current invoking location. * * This is used to prepend to type with a relative location. * May also be 'default' or 'global', in which case they are ignored. * * @var Context */ protected $context = null; /** * Registers the namespace and aliases; uses that to add and expand the * given types. * * @param string[] $types Array containing a list of types to add to this * container. * @param Context $location The current invoking location. */ public function __construct( array $types = array(), Context $context = null ) { $this->context = null === $context ? new Context() : $context; foreach ($types as $type) { $this->add($type); } } /** * Returns the current invoking location. * * @return Context */ public function getContext() { return $this->context; } /** * Adds a new type to the collection and expands it if it contains a * relative namespace. * * If a class in the type contains a relative namespace than this collection * will try to expand that into a FQCN. * * @param string $type A 'Type' as defined in the phpDocumentor * documentation. * * @throws \InvalidArgumentException if a non-string argument is passed. * * @see http://phpdoc.org/docs/latest/for-users/types.html for the * definition of a type. * * @return void */ public function add($type) { if (!is_string($type)) { throw new \InvalidArgumentException( 'A type should be represented by a string, received: ' .var_export($type, true) ); } // separate the type by the OR operator $type_parts = explode(self::OPERATOR_OR, $type); foreach ($type_parts as $part) { $expanded_type = $this->expand($part); if ($expanded_type) { $this[] = $expanded_type; } } } /** * Returns a string representation of the collection. * * @return string The resolved types across the collection, separated with * {@link self::OPERATOR_OR}. */ public function __toString() { return implode(self::OPERATOR_OR, $this->getArrayCopy()); } /** * Analyzes the given type and returns the FQCN variant. * * When a type is provided this method checks whether it is not a keyword or * Fully Qualified Class Name. If so it will use the given namespace and * aliases to expand the type to a FQCN representation. * * This method only works as expected if the namespace and aliases are set; * no dynamic reflection is being performed here. * * @param string $type The relative or absolute type. * * @uses getNamespace to determine with what to prefix the type name. * @uses getNamespaceAliases to check whether the first part of the relative * type name should not be replaced with another namespace. * * @return string */ protected function expand($type) { $type = trim($type); if (!$type) { return ''; } if ($this->isTypeAnArray($type)) { return $this->expand(substr($type, 0, -2)) . self::OPERATOR_ARRAY; } if ($this->isRelativeType($type) && !$this->isTypeAKeyword($type)) { $type_parts = explode(self::OPERATOR_NAMESPACE, $type, 2); $namespace_aliases = $this->context->getNamespaceAliases(); // if the first segment is not an alias; prepend namespace name and // return if (!isset($namespace_aliases[$type_parts[0]])) { $namespace = $this->context->getNamespace(); if ('' !== $namespace) { $namespace .= self::OPERATOR_NAMESPACE; } return self::OPERATOR_NAMESPACE . $namespace . $type; } $type_parts[0] = $namespace_aliases[$type_parts[0]]; $type = implode(self::OPERATOR_NAMESPACE, $type_parts); } return $type; } /** * Detects whether the given type represents an array. * * @param string $type A relative or absolute type as defined in the * phpDocumentor documentation. * * @return bool */ protected function isTypeAnArray($type) { return substr($type, -2) === self::OPERATOR_ARRAY; } /** * Detects whether the given type represents a PHPDoc keyword. * * @param string $type A relative or absolute type as defined in the * phpDocumentor documentation. * * @return bool */ protected function isTypeAKeyword($type) { return in_array(strtolower($type), static::$keywords, true); } /** * Detects whether the given type represents a relative or absolute path. * * This method will detect keywords as being absolute; even though they are * not preceeded by a namespace separator. * * @param string $type A relative or absolute type as defined in the * phpDocumentor documentation. * * @return bool */ protected function isRelativeType($type) { return ($type[0] !== self::OPERATOR_NAMESPACE) || $this->isTypeAKeyword($type); } } * @copyright 2010-2011 Mike van Riel / Naenius (http://www.naenius.com) * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ namespace phpDocumentor\Reflection; use phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Context; use phpDocumentor\Reflection\DocBlock\Location; /** * Parses the DocBlock for any structure. * * @author Mike van Riel * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://phpdoc.org */ class DocBlock implements \Reflector { /** @var string The opening line for this docblock. */ protected $short_description = ''; /** * @var DocBlock\Description The actual * description for this docblock. */ protected $long_description = null; /** * @var Tag[] An array containing all * the tags in this docblock; except inline. */ protected $tags = array(); /** @var Context Information about the context of this DocBlock. */ protected $context = null; /** @var Location Information about the location of this DocBlock. */ protected $location = null; /** @var bool Is this DocBlock (the start of) a template? */ protected $isTemplateStart = false; /** @var bool Does this DocBlock signify the end of a DocBlock template? */ protected $isTemplateEnd = false; /** * Parses the given docblock and populates the member fields. * * The constructor may also receive namespace information such as the * current namespace and aliases. This information is used by some tags * (e.g. @return, @param, etc.) to turn a relative Type into a FQCN. * * @param \Reflector|string $docblock A docblock comment (including * asterisks) or reflector supporting the getDocComment method. * @param Context $context The context in which the DocBlock * occurs. * @param Location $location The location within the file that this * DocBlock occurs in. * * @throws \InvalidArgumentException if the given argument does not have the * getDocComment method. */ public function __construct( $docblock, Context $context = null, Location $location = null ) { if (is_object($docblock)) { if (!method_exists($docblock, 'getDocComment')) { throw new \InvalidArgumentException( 'Invalid object passed; the given reflector must support ' . 'the getDocComment method' ); } $docblock = $docblock->getDocComment(); } $docblock = $this->cleanInput($docblock); list($templateMarker, $short, $long, $tags) = $this->splitDocBlock($docblock); $this->isTemplateStart = $templateMarker === '#@+'; $this->isTemplateEnd = $templateMarker === '#@-'; $this->short_description = $short; $this->long_description = new DocBlock\Description($long, $this); $this->parseTags($tags); $this->context = $context; $this->location = $location; } /** * Strips the asterisks from the DocBlock comment. * * @param string $comment String containing the comment text. * * @return string */ protected function cleanInput($comment) { $comment = trim( preg_replace( '#[ \t]*(?:\/\*\*|\*\/|\*)?[ \t]{0,1}(.*)?#u', '$1', $comment ) ); // reg ex above is not able to remove */ from a single line docblock if (substr($comment, -2) == '*/') { $comment = trim(substr($comment, 0, -2)); } // normalize strings $comment = str_replace(array("\r\n", "\r"), "\n", $comment); return $comment; } /** * Splits the DocBlock into a template marker, summary, description and block of tags. * * @param string $comment Comment to split into the sub-parts. * * @author Richard van Velzen (@_richardJ) Special thanks to Richard for the regex responsible for the split. * @author Mike van Riel for extending the regex with template marker support. * * @return string[] containing the template marker (if any), summary, description and a string containing the tags. */ protected function splitDocBlock($comment) { // Performance improvement cheat: if the first character is an @ then only tags are in this DocBlock. This // method does not split tags so we return this verbatim as the fourth result (tags). This saves us the // performance impact of running a regular expression if (strpos($comment, '@') === 0) { return array('', '', '', $comment); } // clears all extra horizontal whitespace from the line endings to prevent parsing issues $comment = preg_replace('/\h*$/Sum', '', $comment); /* * Splits the docblock into a template marker, short description, long description and tags section * * - The template marker is empty, #@+ or #@- if the DocBlock starts with either of those (a newline may * occur after it and will be stripped). * - The short description is started from the first character until a dot is encountered followed by a * newline OR two consecutive newlines (horizontal whitespace is taken into account to consider spacing * errors). This is optional. * - The long description, any character until a new line is encountered followed by an @ and word * characters (a tag). This is optional. * - Tags; the remaining characters * * Big thanks to RichardJ for contributing this Regular Expression */ preg_match( '/ \A # 1. Extract the template marker (?:(\#\@\+|\#\@\-)\n?)? # 2. Extract the summary (?: (?! @\pL ) # The summary may not start with an @ ( [^\n.]+ (?: (?! \. \n | \n{2} ) # End summary upon a dot followed by newline or two newlines [\n.] (?! [ \t]* @\pL ) # End summary when an @ is found as first character on a new line [^\n.]+ # Include anything else )* \.? )? ) # 3. Extract the description (?: \s* # Some form of whitespace _must_ precede a description because a summary must be there (?! @\pL ) # The description may not start with an @ ( [^\n]+ (?: \n+ (?! [ \t]* @\pL ) # End description when an @ is found as first character on a new line [^\n]+ # Include anything else )* ) )? # 4. Extract the tags (anything that follows) (\s+ [\s\S]*)? # everything that follows /ux', $comment, $matches ); array_shift($matches); while (count($matches) < 4) { $matches[] = ''; } return $matches; } /** * Creates the tag objects. * * @param string $tags Tag block to parse. * * @return void */ protected function parseTags($tags) { $result = array(); $tags = trim($tags); if ('' !== $tags) { if ('@' !== $tags[0]) { throw new \LogicException( 'A tag block started with text instead of an actual tag,' . ' this makes the tag block invalid: ' . $tags ); } foreach (explode("\n", $tags) as $tag_line) { if (isset($tag_line[0]) && ($tag_line[0] === '@')) { $result[] = $tag_line; } else { $result[count($result) - 1] .= "\n" . $tag_line; } } // create proper Tag objects foreach ($result as $key => $tag_line) { $result[$key] = Tag::createInstance(trim($tag_line), $this); } } $this->tags = $result; } /** * Gets the text portion of the doc block. * * Gets the text portion (short and long description combined) of the doc * block. * * @return string The text portion of the doc block. */ public function getText() { $short = $this->getShortDescription(); $long = $this->getLongDescription()->getContents(); if ($long) { return "{$short}\n\n{$long}"; } else { return $short; } } /** * Set the text portion of the doc block. * * Sets the text portion (short and long description combined) of the doc * block. * * @param string $docblock The new text portion of the doc block. * * @return $this This doc block. */ public function setText($comment) { list(,$short, $long) = $this->splitDocBlock($comment); $this->short_description = $short; $this->long_description = new DocBlock\Description($long, $this); return $this; } /** * Returns the opening line or also known as short description. * * @return string */ public function getShortDescription() { return $this->short_description; } /** * Returns the full description or also known as long description. * * @return DocBlock\Description */ public function getLongDescription() { return $this->long_description; } /** * Returns whether this DocBlock is the start of a Template section. * * A Docblock may serve as template for a series of subsequent DocBlocks. This is indicated by a special marker * (`#@+`) that is appended directly after the opening `/**` of a DocBlock. * * An example of such an opening is: * * ``` * /**#@+ * * My DocBlock * * / * ``` * * The description and tags (not the summary!) are copied onto all subsequent DocBlocks and also applied to all * elements that follow until another DocBlock is found that contains the closing marker (`#@-`). * * @see self::isTemplateEnd() for the check whether a closing marker was provided. * * @return boolean */ public function isTemplateStart() { return $this->isTemplateStart; } /** * Returns whether this DocBlock is the end of a Template section. * * @see self::isTemplateStart() for a more complete description of the Docblock Template functionality. * * @return boolean */ public function isTemplateEnd() { return $this->isTemplateEnd; } /** * Returns the current context. * * @return Context */ public function getContext() { return $this->context; } /** * Returns the current location. * * @return Location */ public function getLocation() { return $this->location; } /** * Returns the tags for this DocBlock. * * @return Tag[] */ public function getTags() { return $this->tags; } /** * Returns an array of tags matching the given name. If no tags are found * an empty array is returned. * * @param string $name String to search by. * * @return Tag[] */ public function getTagsByName($name) { $result = array(); /** @var Tag $tag */ foreach ($this->getTags() as $tag) { if ($tag->getName() != $name) { continue; } $result[] = $tag; } return $result; } /** * Checks if a tag of a certain type is present in this DocBlock. * * @param string $name Tag name to check for. * * @return bool */ public function hasTag($name) { /** @var Tag $tag */ foreach ($this->getTags() as $tag) { if ($tag->getName() == $name) { return true; } } return false; } /** * Appends a tag at the end of the list of tags. * * @param Tag $tag The tag to add. * * @return Tag The newly added tag. * * @throws \LogicException When the tag belongs to a different DocBlock. */ public function appendTag(Tag $tag) { if (null === $tag->getDocBlock()) { $tag->setDocBlock($this); } if ($tag->getDocBlock() === $this) { $this->tags[] = $tag; } else { throw new \LogicException( 'This tag belongs to a different DocBlock object.' ); } return $tag; } /** * Builds a string representation of this object. * * @todo determine the exact format as used by PHP Reflection and * implement it. * * @return string * @codeCoverageIgnore Not yet implemented */ public static function export() { throw new \Exception('Not yet implemented'); } /** * Returns the exported information (we should use the export static method * BUT this throws an exception at this point). * * @return string * @codeCoverageIgnore Not yet implemented */ public function __toString() { return 'Not yet implemented'; } } beConstructedWith(array(42, 'zet', $object)); $class = get_class($object->getWrappedObject()); $hash = spl_object_hash($object->getWrappedObject()); $this->__toString()->shouldReturn("exact(42), exact(\"zet\"), exact($class:$hash Object (\n 'objectProphecy' => Prophecy\Prophecy\ObjectProphecy Object (*Prophecy*)\n))"); } function it_generates_string_representation_from_all_tokens_imploded( TokenInterface $token1, TokenInterface $token2, TokenInterface $token3 ) { $token1->__toString()->willReturn('token_1'); $token2->__toString()->willReturn('token_2'); $token3->__toString()->willReturn('token_3'); $this->beConstructedWith(array($token1, $token2, $token3)); $this->__toString()->shouldReturn('token_1, token_2, token_3'); } function it_exposes_list_of_tokens(TokenInterface $token) { $this->beConstructedWith(array($token)); $this->getTokens()->shouldReturn(array($token)); } function it_returns_score_of_1_if_there_are_no_tokens_and_arguments() { $this->beConstructedWith(array()); $this->scoreArguments(array())->shouldReturn(1); } function it_should_return_match_score_based_on_all_tokens_score( TokenInterface $token1, TokenInterface $token2, TokenInterface $token3 ) { $token1->scoreArgument('one')->willReturn(3); $token1->isLast()->willReturn(false); $token2->scoreArgument(2)->willReturn(5); $token2->isLast()->willReturn(false); $token3->scoreArgument($obj = new \stdClass())->willReturn(10); $token3->isLast()->willReturn(false); $this->beConstructedWith(array($token1, $token2, $token3)); $this->scoreArguments(array('one', 2, $obj))->shouldReturn(18); } function it_returns_false_if_there_is_less_arguments_than_tokens( TokenInterface $token1, TokenInterface $token2, TokenInterface $token3 ) { $token1->scoreArgument('one')->willReturn(3); $token1->isLast()->willReturn(false); $token2->scoreArgument(2)->willReturn(5); $token2->isLast()->willReturn(false); $token3->scoreArgument(null)->willReturn(false); $token3->isLast()->willReturn(false); $this->beConstructedWith(array($token1, $token2, $token3)); $this->scoreArguments(array('one', 2))->shouldReturn(false); } function it_returns_false_if_there_is_less_tokens_than_arguments( TokenInterface $token1, TokenInterface $token2, TokenInterface $token3 ) { $token1->scoreArgument('one')->willReturn(3); $token1->isLast()->willReturn(false); $token2->scoreArgument(2)->willReturn(5); $token2->isLast()->willReturn(false); $token3->scoreArgument($obj = new \stdClass())->willReturn(10); $token3->isLast()->willReturn(false); $this->beConstructedWith(array($token1, $token2, $token3)); $this->scoreArguments(array('one', 2, $obj, 4))->shouldReturn(false); } function it_should_return_false_if_one_of_the_tokens_returns_false( TokenInterface $token1, TokenInterface $token2, TokenInterface $token3 ) { $token1->scoreArgument('one')->willReturn(3); $token1->isLast()->willReturn(false); $token2->scoreArgument(2)->willReturn(false); $token2->isLast()->willReturn(false); $token3->scoreArgument($obj = new \stdClass())->willReturn(10); $token3->isLast()->willReturn(false); $this->beConstructedWith(array($token1, $token2, $token3)); $this->scoreArguments(array('one', 2, $obj))->shouldReturn(false); } function it_should_calculate_score_until_last_token( TokenInterface $token1, TokenInterface $token2, TokenInterface $token3 ) { $token1->scoreArgument('one')->willReturn(3); $token1->isLast()->willReturn(false); $token2->scoreArgument(2)->willReturn(7); $token2->isLast()->willReturn(true); $token3->scoreArgument($obj = new \stdClass())->willReturn(10); $token3->isLast()->willReturn(false); $this->beConstructedWith(array($token1, $token2, $token3)); $this->scoreArguments(array('one', 2, $obj))->shouldReturn(10); } } shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_last() { $this->shouldBeLast(); } function its_string_representation_is_star_with_followup() { $this->__toString()->shouldReturn('* [, ...]'); } function it_scores_any_argument_as_2() { $this->scoreArgument(42)->shouldReturn(2); } } shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_not_last() { $this->shouldNotBeLast(); } function its_string_representation_is_star() { $this->__toString()->shouldReturn('*'); } function it_scores_any_argument_as_3() { $this->scoreArgument(42)->shouldReturn(3); } } beConstructedWith(10.12345678, 4); } function it_is_initializable() { $this->shouldHaveType('Prophecy\Argument\Token\ApproximateValueToken'); } function it_implements_TokenInterface() { $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_not_last() { $this->shouldNotBeLast(); } function it_scores_10_if_rounded_argument_matches_rounded_value() { $this->scoreArgument(10.12345)->shouldReturn(10); } function it_does_not_score_if_rounded_argument_does_not_match_rounded_value() { $this->scoreArgument(10.1234)->shouldReturn(false); } function it_uses_a_default_precision_of_zero() { $this->beConstructedWith(10.7); $this->scoreArgument(11.4)->shouldReturn(10); } function it_does_not_score_if_rounded_argument_is_not_numeric() { $this->scoreArgument('hello')->shouldReturn(false); } function it_has_simple_string_representation() { $this->__toString()->shouldBe('≅10.1235'); } } beConstructedWith(2); } function it_implements_TokenInterface() { $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_not_last() { $this->shouldNotBeLast(); } function it_scores_6_if_argument_array_has_proper_count() { $this->scoreArgument(array(1,2))->shouldReturn(6); } function it_scores_6_if_argument_countable_object_has_proper_count(\Countable $countable) { $countable->count()->willReturn(2); $this->scoreArgument($countable)->shouldReturn(6); } function it_does_not_score_if_argument_is_neither_array_nor_countable_object() { $this->scoreArgument('string')->shouldBe(false); $this->scoreArgument(5)->shouldBe(false); $this->scoreArgument(new \stdClass)->shouldBe(false); } function it_does_not_score_if_argument_array_has_wrong_count() { $this->scoreArgument(array(1))->shouldReturn(false); } function it_does_not_score_if_argument_countable_object_has_wrong_count(\Countable $countable) { $countable->count()->willReturn(3); $this->scoreArgument($countable)->shouldReturn(false); } function it_has_simple_string_representation() { $this->__toString()->shouldBe('count(2)'); } } beConstructedWith($key, $value); } function it_implements_TokenInterface() { $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_not_last() { $this->shouldNotBeLast(); } function it_holds_key_and_value($key, $value) { $this->getKey()->shouldBe($key); $this->getValue()->shouldBe($value); } function its_string_representation_tells_that_its_an_array_containing_the_key_value_pair($key, $value) { $key->__toString()->willReturn('key'); $value->__toString()->willReturn('value'); $this->__toString()->shouldBe('[..., key => value, ...]'); } function it_wraps_non_token_value_into_ExactValueToken(TokenInterface $key, \stdClass $object) { $this->beConstructedWith($key, $object); $this->getValue()->shouldHaveType('\Prophecy\Argument\Token\ExactValueToken'); } function it_wraps_non_token_key_into_ExactValueToken(\stdClass $object, TokenInterface $value) { $this->beConstructedWith($object, $value); $this->getKey()->shouldHaveType('\Prophecy\Argument\Token\ExactValueToken'); } function it_scores_array_half_of_combined_scores_from_key_and_value_tokens($key, $value) { $key->scoreArgument('key')->willReturn(4); $value->scoreArgument('value')->willReturn(6); $this->scoreArgument(array('key'=>'value'))->shouldBe(5); } function it_scores_traversable_object_half_of_combined_scores_from_key_and_value_tokens( TokenInterface $key, TokenInterface $value, \Iterator $object ) { $object->current()->will(function () use ($object) { $object->valid()->willReturn(false); return 'value'; }); $object->key()->willReturn('key'); $object->rewind()->willReturn(null); $object->next()->willReturn(null); $object->valid()->willReturn(true); $key->scoreArgument('key')->willReturn(6); $value->scoreArgument('value')->willReturn(2); $this->scoreArgument($object)->shouldBe(4); } function it_throws_exception_during_scoring_of_array_accessible_object_if_key_is_not_ExactValueToken( TokenInterface $key, TokenInterface $value, \ArrayAccess $object ) { $key->__toString()->willReturn('any_token'); $this->beConstructedWith($key,$value); $errorMessage = 'You can only use exact value tokens to match key of ArrayAccess object'.PHP_EOL. 'But you used `any_token`.'; $this->shouldThrow(new InvalidArgumentException($errorMessage))->duringScoreArgument($object); } function it_scores_array_accessible_object_half_of_combined_scores_from_key_and_value_tokens( ExactValueToken $key, TokenInterface $value, \ArrayAccess $object ) { $object->offsetExists('key')->willReturn(true); $object->offsetGet('key')->willReturn('value'); $key->getValue()->willReturn('key'); $key->scoreArgument('key')->willReturn(3); $value->scoreArgument('value')->willReturn(1); $this->scoreArgument($object)->shouldBe(2); } function it_accepts_any_key_token_type_to_score_object_that_is_both_traversable_and_array_accessible( TokenInterface $key, TokenInterface $value, \ArrayIterator $object ) { $this->beConstructedWith($key, $value); $object->current()->will(function () use ($object) { $object->valid()->willReturn(false); return 'value'; }); $object->key()->willReturn('key'); $object->rewind()->willReturn(null); $object->next()->willReturn(null); $object->valid()->willReturn(true); $this->shouldNotThrow(new InvalidArgumentException)->duringScoreArgument($object); } function it_does_not_score_if_argument_is_neither_array_nor_traversable_nor_array_accessible() { $this->scoreArgument('string')->shouldBe(false); $this->scoreArgument(new \stdClass)->shouldBe(false); } function it_does_not_score_empty_array() { $this->scoreArgument(array())->shouldBe(false); } function it_does_not_score_array_if_key_and_value_tokens_do_not_score_same_entry($key, $value) { $argument = array(1 => 'foo', 2 => 'bar'); $key->scoreArgument(1)->willReturn(true); $key->scoreArgument(2)->willReturn(false); $value->scoreArgument('foo')->willReturn(false); $value->scoreArgument('bar')->willReturn(true); $this->scoreArgument($argument)->shouldBe(false); } function it_does_not_score_traversable_object_without_entries(\Iterator $object) { $object->rewind()->willReturn(null); $object->next()->willReturn(null); $object->valid()->willReturn(false); $this->scoreArgument($object)->shouldBe(false); } function it_does_not_score_traversable_object_if_key_and_value_tokens_do_not_score_same_entry( TokenInterface $key, TokenInterface $value, \Iterator $object ) { $object->current()->willReturn('foo'); $object->current()->will(function () use ($object) { $object->valid()->willReturn(false); return 'bar'; }); $object->key()->willReturn(1); $object->key()->willReturn(2); $object->rewind()->willReturn(null); $object->next()->willReturn(null); $object->valid()->willReturn(true); $key->scoreArgument(1)->willReturn(true); $key->scoreArgument(2)->willReturn(false); $value->scoreArgument('foo')->willReturn(false); $value->scoreArgument('bar')->willReturn(true); $this->scoreArgument($object)->shouldBe(false); } function it_does_not_score_array_accessible_object_if_it_has_no_offset_with_key_token_value( ExactValueToken $key, \ArrayAccess $object ) { $object->offsetExists('key')->willReturn(false); $key->getValue()->willReturn('key'); $this->scoreArgument($object)->shouldBe(false); } function it_does_not_score_array_accessible_object_if_key_and_value_tokens_do_not_score_same_entry( ExactValueToken $key, TokenInterface $value, \ArrayAccess $object ) { $object->offsetExists('key')->willReturn(true); $object->offsetGet('key')->willReturn('value'); $key->getValue()->willReturn('key'); $value->scoreArgument('value')->willReturn(false); $key->scoreArgument('key')->willReturn(true); $this->scoreArgument($object)->shouldBe(false); } function its_score_is_capped_at_8($key, $value) { $key->scoreArgument('key')->willReturn(10); $value->scoreArgument('value')->willReturn(10); $this->scoreArgument(array('key'=>'value'))->shouldBe(8); } } beConstructedWith($value); } function it_implements_TokenInterface() { $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_not_last() { $this->shouldNotBeLast(); } function it_holds_value($value) { $this->getValue()->shouldBe($value); } function its_string_representation_tells_that_its_an_array_containing_only_value($value) { $value->__toString()->willReturn('value'); $this->__toString()->shouldBe('[value, ..., value]'); } function it_wraps_non_token_value_into_ExactValueToken(\stdClass $stdClass) { $this->beConstructedWith($stdClass); $this->getValue()->shouldHaveType('Prophecy\Argument\Token\ExactValueToken'); } function it_does_not_score_if_argument_is_neither_array_nor_traversable() { $this->scoreArgument('string')->shouldBe(false); $this->scoreArgument(new \stdClass)->shouldBe(false); } function it_does_not_score_empty_array() { $this->scoreArgument(array())->shouldBe(false); } function it_does_not_score_traversable_object_without_entries(\Iterator $object) { $object->rewind()->willReturn(null); $object->next()->willReturn(null); $object->valid()->willReturn(false); $this->scoreArgument($object)->shouldBe(false); } function it_scores_avg_of_scores_from_value_tokens($value) { $value->scoreArgument('value1')->willReturn(6); $value->scoreArgument('value2')->willReturn(3); $this->scoreArgument(array('value1', 'value2'))->shouldBe(4.5); } function it_scores_false_if_entry_scores_false($value) { $value->scoreArgument('value1')->willReturn(6); $value->scoreArgument('value2')->willReturn(false); $this->scoreArgument(array('value1', 'value2'))->shouldBe(false); } function it_does_not_score_array_keys($value) { $value->scoreArgument('value')->willReturn(6); $value->scoreArgument('key')->shouldNotBeCalled(0); $this->scoreArgument(array('key' => 'value'))->shouldBe(6); } function it_scores_traversable_object_from_value_token(TokenInterface $value, \Iterator $object) { $object->current()->will(function ($args, $object) { $object->valid()->willReturn(false); return 'value'; }); $object->key()->willReturn('key'); $object->rewind()->willReturn(null); $object->next()->willReturn(null); $object->valid()->willReturn(true); $value->scoreArgument('value')->willReturn(2); $this->scoreArgument($object)->shouldBe(2); } } beConstructedWith('get_class'); } function it_implements_TokenInterface() { $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_not_last() { $this->shouldNotBeLast(); } function it_scores_7_if_argument_matches_callback() { $this->beConstructedWith(function ($argument) { return 2 === $argument; }); $this->scoreArgument(2)->shouldReturn(7); } function it_does_not_scores_if_argument_does_not_match_callback() { $this->beConstructedWith(function ($argument) { return 2 === $argument; }); $this->scoreArgument(5)->shouldReturn(false); } function its_string_representation_should_tell_that_its_callback() { $this->__toString()->shouldReturn('callback()'); } } beConstructedWith(42); } function it_implements_TokenInterface() { $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_not_last() { $this->shouldNotBeLast(); } function it_holds_value() { $this->getValue()->shouldReturn(42); } function it_scores_10_if_value_is_equal_to_argument() { $this->scoreArgument(42)->shouldReturn(10); $this->scoreArgument('42')->shouldReturn(10); } function it_scores_10_if_value_is_an_object_and_equal_to_argument() { $value = new \DateTime(); $value2 = clone $value; $this->beConstructedWith($value); $this->scoreArgument($value2)->shouldReturn(10); } function it_does_not_scores_if_value_is_not_equal_to_argument() { $this->scoreArgument(50)->shouldReturn(false); $this->scoreArgument(new \stdClass())->shouldReturn(false); } function it_does_not_scores_if_value_an_object_and_is_not_equal_to_argument() { $value = new ExactValueTokenFixtureB('ABC'); $value2 = new ExactValueTokenFixtureB('CBA'); $this->beConstructedWith($value); $this->scoreArgument($value2)->shouldReturn(false); } function it_does_not_scores_if_value_type_and_is_not_equal_to_argument() { $this->beConstructedWith(false); $this->scoreArgument(0)->shouldReturn(false); } function it_generates_proper_string_representation_for_integer() { $this->beConstructedWith(42); $this->__toString()->shouldReturn('exact(42)'); } function it_generates_proper_string_representation_for_string() { $this->beConstructedWith('some string'); $this->__toString()->shouldReturn('exact("some string")'); } function it_generates_single_line_representation_for_multiline_string() { $this->beConstructedWith("some\nstring"); $this->__toString()->shouldReturn('exact("some\\nstring")'); } function it_generates_proper_string_representation_for_double() { $this->beConstructedWith(42.3); $this->__toString()->shouldReturn('exact(42.3)'); } function it_generates_proper_string_representation_for_boolean_true() { $this->beConstructedWith(true); $this->__toString()->shouldReturn('exact(true)'); } function it_generates_proper_string_representation_for_boolean_false() { $this->beConstructedWith(false); $this->__toString()->shouldReturn('exact(false)'); } function it_generates_proper_string_representation_for_null() { $this->beConstructedWith(null); $this->__toString()->shouldReturn('exact(null)'); } function it_generates_proper_string_representation_for_empty_array() { $this->beConstructedWith(array()); $this->__toString()->shouldReturn('exact([])'); } function it_generates_proper_string_representation_for_array() { $this->beConstructedWith(array('zet', 42)); $this->__toString()->shouldReturn('exact(["zet", 42])'); } function it_generates_proper_string_representation_for_resource() { $resource = fopen(__FILE__, 'r'); $this->beConstructedWith($resource); $this->__toString()->shouldReturn('exact(stream:'.$resource.')'); } function it_generates_proper_string_representation_for_object(\stdClass $object) { $objHash = sprintf('%s:%s', get_class($object->getWrappedObject()), spl_object_hash($object->getWrappedObject()) ); $this->beConstructedWith($object); $this->__toString()->shouldReturn("exact($objHash Object (\n 'objectProphecy' => Prophecy\Prophecy\ObjectProphecy Object (*Prophecy*)\n))"); } } class ExactValueTokenFixtureA { public $errors; } class ExactValueTokenFixtureB extends ExactValueTokenFixtureA { public $errors; public $value = null; public function __construct($value) { $this->value = $value; } } beConstructedWith(42); } function it_is_initializable() { $this->shouldHaveType('Prophecy\Argument\Token\IdenticalValueToken'); } function it_scores_11_if_string_value_is_identical_to_argument() { $this->beConstructedWith('foo'); $this->scoreArgument('foo')->shouldReturn(11); } function it_scores_11_if_boolean_value_is_identical_to_argument() { $this->beConstructedWith(false); $this->scoreArgument(false)->shouldReturn(11); } function it_scores_11_if_integer_value_is_identical_to_argument() { $this->beConstructedWith(31); $this->scoreArgument(31)->shouldReturn(11); } function it_scores_11_if_float_value_is_identical_to_argument() { $this->beConstructedWith(31.12); $this->scoreArgument(31.12)->shouldReturn(11); } function it_scores_11_if_array_value_is_identical_to_argument() { $this->beConstructedWith(array('foo' => 'bar')); $this->scoreArgument(array('foo' => 'bar'))->shouldReturn(11); } function it_scores_11_if_object_value_is_identical_to_argument() { $object = new \stdClass(); $this->beConstructedWith($object); $this->scoreArgument($object)->shouldReturn(11); } function it_scores_false_if_value_is_not_identical_to_argument() { $this->beConstructedWith(new \stdClass()); $this->scoreArgument('foo')->shouldReturn(false); } function it_scores_false_if_object_value_is_not_the_same_instance_than_argument() { $this->beConstructedWith(new \stdClass()); $this->scoreArgument(new \stdClass())->shouldReturn(false); } function it_scores_false_if_integer_value_is_not_identical_to_boolean_argument() { $this->beConstructedWith(1); $this->scoreArgument(true)->shouldReturn(false); } function it_is_not_last() { $this->shouldNotBeLast(); } function it_generates_proper_string_representation_for_integer() { $this->beConstructedWith(42); $this->__toString()->shouldReturn('identical(42)'); } function it_generates_proper_string_representation_for_string() { $this->beConstructedWith('some string'); $this->__toString()->shouldReturn('identical("some string")'); } function it_generates_single_line_representation_for_multiline_string() { $this->beConstructedWith("some\nstring"); $this->__toString()->shouldReturn('identical("some\\nstring")'); } function it_generates_proper_string_representation_for_double() { $this->beConstructedWith(42.3); $this->__toString()->shouldReturn('identical(42.3)'); } function it_generates_proper_string_representation_for_boolean_true() { $this->beConstructedWith(true); $this->__toString()->shouldReturn('identical(true)'); } function it_generates_proper_string_representation_for_boolean_false() { $this->beConstructedWith(false); $this->__toString()->shouldReturn('identical(false)'); } function it_generates_proper_string_representation_for_null() { $this->beConstructedWith(null); $this->__toString()->shouldReturn('identical(null)'); } function it_generates_proper_string_representation_for_empty_array() { $this->beConstructedWith(array()); $this->__toString()->shouldReturn('identical([])'); } function it_generates_proper_string_representation_for_array() { $this->beConstructedWith(array('zet', 42)); $this->__toString()->shouldReturn('identical(["zet", 42])'); } function it_generates_proper_string_representation_for_resource() { $resource = fopen(__FILE__, 'r'); $this->beConstructedWith($resource); $this->__toString()->shouldReturn('identical(stream:'.$resource.')'); } function it_generates_proper_string_representation_for_object($object) { $objHash = sprintf('%s:%s', get_class($object->getWrappedObject()), spl_object_hash($object->getWrappedObject()) ); $this->beConstructedWith($object); $this->__toString()->shouldReturn("identical($objHash Object (\n 'objectProphecy' => Prophecy\Prophecy\ObjectProphecy Object (*Prophecy*)\n))"); } } beConstructedWith(array()); $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_not_last() { $this->beConstructedWith(array()); $this->shouldNotBeLast(); } function it_generates_string_representation_from_all_tokens_imploded( TokenInterface $token1, TokenInterface $token2, TokenInterface $token3 ) { $token1->__toString()->willReturn('token_1'); $token2->__toString()->willReturn('token_2'); $token3->__toString()->willReturn('token_3'); $this->beConstructedWith(array($token1, $token2, $token3)); $this->__toString()->shouldReturn('bool(token_1 AND token_2 AND token_3)'); } function it_wraps_non_token_arguments_into_ExactValueToken() { $this->beConstructedWith(array(15, '1985')); $this->__toString()->shouldReturn("bool(exact(15) AND exact(\"1985\"))"); } function it_scores_the_maximum_score_from_all_scores_returned_by_tokens(TokenInterface $token1, TokenInterface $token2) { $token1->scoreArgument(1)->willReturn(10); $token2->scoreArgument(1)->willReturn(5); $this->beConstructedWith(array($token1, $token2)); $this->scoreArgument(1)->shouldReturn(10); } function it_does_not_score_if_there_are_no_arguments_or_tokens() { $this->beConstructedWith(array()); $this->scoreArgument('any')->shouldReturn(false); } function it_does_not_score_if_either_of_tokens_does_not_score(TokenInterface $token1, TokenInterface $token2) { $token1->scoreArgument(1)->willReturn(10); $token1->scoreArgument(2)->willReturn(false); $token2->scoreArgument(1)->willReturn(false); $token2->scoreArgument(2)->willReturn(10); $this->beConstructedWith(array($token1, $token2)); $this->scoreArgument(1)->shouldReturn(false); $this->scoreArgument(2)->shouldReturn(false); } } beConstructedWith($token); } function it_implements_TokenInterface() { $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_holds_originating_token($token) { $this->getOriginatingToken()->shouldReturn($token); } function it_has_simple_string_representation($token) { $token->__toString()->willReturn('value'); $this->__toString()->shouldBe('not(value)'); } function it_wraps_non_token_argument_into_ExactValueToken() { $this->beConstructedWith(5); $token = $this->getOriginatingToken(); $token->shouldhaveType('Prophecy\Argument\Token\ExactValueToken'); $token->getValue()->shouldBe(5); } function it_scores_4_if_preset_token_does_not_match_the_argument($token) { $token->scoreArgument('argument')->willReturn(false); $this->scoreArgument('argument')->shouldBe(4); } function it_does_not_score_if_preset_token_matches_argument($token) { $token->scoreArgument('argument')->willReturn(5); $this->scoreArgument('argument')->shouldBe(false); } function it_is_last_if_preset_token_is_last($token) { $token->isLast()->willReturn(true); $this->shouldBeLast(); } function it_is_not_last_if_preset_token_is_not_last($token) { $token->isLast()->willReturn(false); $this->shouldNotBeLast(); } } beConstructedWith('getName', 'stdClass'); } function it_implements_TokenInterface() { $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_not_last() { $this->shouldNotBeLast(); } function it_scores_8_if_argument_object_has_specific_method_state(\ReflectionClass $reflection) { $reflection->getName()->willReturn('stdClass'); $this->scoreArgument($reflection)->shouldReturn(8); } function it_scores_8_if_argument_object_has_specific_property_state(\stdClass $class) { $class->getName = 'stdClass'; $this->scoreArgument($class)->shouldReturn(8); } function it_does_not_score_if_argument_method_state_does_not_match() { $value = new ObjectStateTokenFixtureB('ABC'); $value2 = new ObjectStateTokenFixtureB('CBA'); $this->beConstructedWith('getSelf', $value); $this->scoreArgument($value2)->shouldReturn(false); } function it_does_not_score_if_argument_property_state_does_not_match(\stdClass $class) { $class->getName = 'SplFileInfo'; $this->scoreArgument($class)->shouldReturn(false); } function it_does_not_score_if_argument_object_does_not_have_method_or_property(ObjectStateTokenFixtureA $class) { $this->scoreArgument($class)->shouldReturn(false); } function it_does_not_score_if_argument_is_not_object() { $this->scoreArgument(42)->shouldReturn(false); } function it_has_simple_string_representation() { $this->__toString()->shouldReturn('state(getName(), "stdClass")'); } } class ObjectStateTokenFixtureA { public $errors; } class ObjectStateTokenFixtureB extends ObjectStateTokenFixtureA { public $errors; public $value = null; public function __construct($value) { $this->value = $value; } public function getSelf() { return $this; } } beConstructedWith('a substring'); } function it_is_initializable() { $this->shouldHaveType('Prophecy\Argument\Token\StringContainsToken'); } function it_implements_TokenInterface() { $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_holds_value() { $this->getValue()->shouldReturn('a substring'); } function it_is_not_last() { $this->shouldNotBeLast(); } function it_scores_6_if_the_argument_contains_the_value() { $this->scoreArgument('Argument containing a substring')->shouldReturn(6); } function it_does_not_score_if_the_argument_does_not_contain_the_value() { $this->scoreArgument('Argument will not match')->shouldReturn(false); } function its_string_representation_shows_substring() { $this->__toString()->shouldReturn('contains("a substring")'); } } beConstructedWith('integer'); } function it_implements_TokenInterface() { $this->shouldBeAnInstanceOf('Prophecy\Argument\Token\TokenInterface'); } function it_is_not_last() { $this->shouldNotBeLast(); } function it_scores_5_if_argument_matches_simple_type() { $this->beConstructedWith('integer'); $this->scoreArgument(42)->shouldReturn(5); } function it_does_not_scores_if_argument_does_not_match_simple_type() { $this->beConstructedWith('integer'); $this->scoreArgument(42.0)->shouldReturn(false); } function it_scores_5_if_argument_is_an_instance_of_specified_class(\ReflectionObject $object) { $this->beConstructedWith('ReflectionClass'); $this->scoreArgument($object)->shouldReturn(5); } function it_has_simple_string_representation() { $this->__toString()->shouldReturn('type(integer)'); } function it_scores_5_if_argument_is_an_instance_of_specified_interface(TokenInterface $interface) { $this->beConstructedWith('Prophecy\Argument\Token\TokenInterface'); $this->scoreArgument($interface)->shouldReturn(5); } } exact(42); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\ExactValueToken'); $token->getValue()->shouldReturn(42); } function it_has_a_shortcut_for_any_argument_token() { $token = $this->any(); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\AnyValueToken'); } function it_has_a_shortcut_for_multiple_arguments_token() { $token = $this->cetera(); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\AnyValuesToken'); } function it_has_a_shortcut_for_type_token() { $token = $this->type('integer'); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\TypeToken'); } function it_has_a_shortcut_for_callback_token() { $token = $this->that('get_class'); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\CallbackToken'); } function it_has_a_shortcut_for_object_state_token() { $token = $this->which('getName', 'everzet'); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\ObjectStateToken'); } function it_has_a_shortcut_for_logical_and_token() { $token = $this->allOf('integer', 5); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\LogicalAndToken'); } function it_has_a_shortcut_for_array_count_token() { $token = $this->size(5); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\ArrayCountToken'); } function it_has_a_shortcut_for_array_entry_token() { $token = $this->withEntry('key', 'value'); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\ArrayEntryToken'); } function it_has_a_shortcut_for_array_every_entry_token() { $token = $this->withEveryEntry('value'); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\ArrayEveryEntryToken'); } function it_has_a_shortcut_for_identical_value_token() { $token = $this->is('value'); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\IdenticalValueToken'); } function it_has_a_shortcut_for_array_entry_token_matching_any_key() { $token = $this->containing('value'); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\ArrayEntryToken'); $token->getKey()->shouldHaveType('Prophecy\Argument\Token\AnyValueToken'); } function it_has_a_shortcut_for_array_entry_token_matching_any_value() { $token = $this->withKey('key'); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\ArrayEntryToken'); $token->getValue()->shouldHaveType('Prophecy\Argument\Token\AnyValueToken'); } function it_has_a_shortcut_for_logical_not_token() { $token = $this->not('kagux'); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\LogicalNotToken'); } function it_has_a_shortcut_for_string_contains_token() { $token = $this->containingString('string'); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\StringContainsToken'); } function it_has_a_shortcut_for_approximate_token() { $token = $this->approximate(10); $token->shouldBeAnInstanceOf('Prophecy\Argument\Token\ApproximateValueToken'); } } scoreArguments(array(5, 2, 3))->willReturn(10); $objectProphecy->getMethodProphecies()->willReturn(array()); $this->makeCall($objectProphecy, 'setValues', array(5, 2, 3)); $calls = $this->findCalls('setValues', $wildcard); $calls->shouldHaveCount(1); $calls[0]->shouldBeAnInstanceOf('Prophecy\Call\Call'); $calls[0]->getMethodName()->shouldReturn('setValues'); $calls[0]->getArguments()->shouldReturn(array(5, 2, 3)); $calls[0]->getReturnValue()->shouldReturn(null); } function it_returns_null_for_any_call_through_makeCall_if_no_method_prophecies_added( $objectProphecy ) { $objectProphecy->getMethodProphecies()->willReturn(array()); $this->makeCall($objectProphecy, 'setValues', array(5, 2, 3))->shouldReturn(null); } function it_executes_promise_of_method_prophecy_that_matches_signature_passed_to_makeCall( $objectProphecy, MethodProphecy $method1, MethodProphecy $method2, MethodProphecy $method3, ArgumentsWildcard $arguments1, ArgumentsWildcard $arguments2, ArgumentsWildcard $arguments3, PromiseInterface $promise ) { $method1->getMethodName()->willReturn('getName'); $method1->getArgumentsWildcard()->willReturn($arguments1); $arguments1->scoreArguments(array('world', 'everything'))->willReturn(false); $method2->getMethodName()->willReturn('setTitle'); $method2->getArgumentsWildcard()->willReturn($arguments2); $arguments2->scoreArguments(array('world', 'everything'))->willReturn(false); $method3->getMethodName()->willReturn('getName'); $method3->getArgumentsWildcard()->willReturn($arguments3); $method3->getPromise()->willReturn($promise); $arguments3->scoreArguments(array('world', 'everything'))->willReturn(200); $objectProphecy->getMethodProphecies()->willReturn(array( 'method1' => array($method1), 'method2' => array($method2, $method3) )); $objectProphecy->getMethodProphecies('getName')->willReturn(array($method1, $method3)); $objectProphecy->reveal()->willReturn(new \stdClass()); $promise->execute(array('world', 'everything'), $objectProphecy->getWrappedObject(), $method3)->willReturn(42); $this->makeCall($objectProphecy, 'getName', array('world', 'everything'))->shouldReturn(42); $calls = $this->findCalls('getName', $arguments3); $calls->shouldHaveCount(1); $calls[0]->getReturnValue()->shouldReturn(42); } function it_executes_promise_of_method_prophecy_that_matches_with_highest_score_to_makeCall( $objectProphecy, MethodProphecy $method1, MethodProphecy $method2, MethodProphecy $method3, ArgumentsWildcard $arguments1, ArgumentsWildcard $arguments2, ArgumentsWildcard $arguments3, PromiseInterface $promise ) { $method1->getMethodName()->willReturn('getName'); $method1->getArgumentsWildcard()->willReturn($arguments1); $arguments1->scoreArguments(array('world', 'everything'))->willReturn(50); $method2->getMethodName()->willReturn('getName'); $method2->getArgumentsWildcard()->willReturn($arguments2); $method2->getPromise()->willReturn($promise); $arguments2->scoreArguments(array('world', 'everything'))->willReturn(300); $method3->getMethodName()->willReturn('getName'); $method3->getArgumentsWildcard()->willReturn($arguments3); $arguments3->scoreArguments(array('world', 'everything'))->willReturn(200); $objectProphecy->getMethodProphecies()->willReturn(array( 'method1' => array($method1), 'method2' => array($method2, $method3) )); $objectProphecy->getMethodProphecies('getName')->willReturn(array( $method1, $method2, $method3 )); $objectProphecy->reveal()->willReturn(new \stdClass()); $promise->execute(array('world', 'everything'), $objectProphecy->getWrappedObject(), $method2) ->willReturn('second'); $this->makeCall($objectProphecy, 'getName', array('world', 'everything')) ->shouldReturn('second'); } function it_throws_exception_if_call_does_not_match_any_of_defined_method_prophecies( $objectProphecy, MethodProphecy $method, ArgumentsWildcard $arguments ) { $method->getMethodName()->willReturn('getName'); $method->getArgumentsWildcard()->willReturn($arguments); $arguments->scoreArguments(array('world', 'everything'))->willReturn(false); $arguments->__toString()->willReturn('arg1, arg2'); $objectProphecy->getMethodProphecies()->willReturn(array('method1' => array($method))); $objectProphecy->getMethodProphecies('getName')->willReturn(array($method)); $this->shouldThrow('Prophecy\Exception\Call\UnexpectedCallException') ->duringMakeCall($objectProphecy, 'getName', array('world', 'everything')); } function it_returns_null_if_method_prophecy_that_matches_makeCall_arguments_has_no_promise( $objectProphecy, MethodProphecy $method, ArgumentsWildcard $arguments ) { $method->getMethodName()->willReturn('getName'); $method->getArgumentsWildcard()->willReturn($arguments); $method->getPromise()->willReturn(null); $arguments->scoreArguments(array('world', 'everything'))->willReturn(100); $objectProphecy->getMethodProphecies()->willReturn(array($method)); $objectProphecy->getMethodProphecies('getName')->willReturn(array($method)); $this->makeCall($objectProphecy, 'getName', array('world', 'everything')) ->shouldReturn(null); } function it_finds_recorded_calls_by_a_method_name_and_arguments_wildcard( $objectProphecy, ArgumentsWildcard $wildcard ) { $objectProphecy->getMethodProphecies()->willReturn(array()); $this->makeCall($objectProphecy, 'getName', array('world')); $this->makeCall($objectProphecy, 'getName', array('everything')); $this->makeCall($objectProphecy, 'setName', array(42)); $wildcard->scoreArguments(array('world'))->willReturn(false); $wildcard->scoreArguments(array('everything'))->willReturn(10); $calls = $this->findCalls('getName', $wildcard); $calls->shouldHaveCount(1); $calls[0]->getMethodName()->shouldReturn('getName'); $calls[0]->getArguments()->shouldReturn(array('everything')); } } beConstructedWith('setValues', array(5, 2), 42, $exception, 'some_file.php', 23); } function it_exposes_method_name_through_getter() { $this->getMethodName()->shouldReturn('setValues'); } function it_exposes_arguments_through_getter() { $this->getArguments()->shouldReturn(array(5, 2)); } function it_exposes_return_value_through_getter() { $this->getReturnValue()->shouldReturn(42); } function it_exposes_exception_through_getter($exception) { $this->getException()->shouldReturn($exception); } function it_exposes_file_and_line_through_getter() { $this->getFile()->shouldReturn('some_file.php'); $this->getLine()->shouldReturn(23); } function it_returns_shortpath_to_callPlace() { $this->getCallPlace()->shouldReturn('some_file.php:23'); } function it_returns_unknown_as_callPlace_if_no_file_or_line_provided() { $this->beConstructedWith('setValues', array(), 0, null, null, null); $this->getCallPlace()->shouldReturn('unknown'); } } shouldHaveType('SebastianBergmann\Comparator\Comparator'); } function it_accepts_only_closures() { $this->accepts(123, 321)->shouldReturn(false); $this->accepts('string', 'string')->shouldReturn(false); $this->accepts(false, true)->shouldReturn(false); $this->accepts(true, false)->shouldReturn(false); $this->accepts((object)array(), (object)array())->shouldReturn(false); $this->accepts(function(){}, (object)array())->shouldReturn(false); $this->accepts(function(){}, (object)array())->shouldReturn(false); $this->accepts(function(){}, function(){})->shouldReturn(true); } function it_asserts_that_all_closures_are_different() { $this->shouldThrow()->duringAssertEquals(function(){}, function(){}); } function it_asserts_that_all_closures_are_different_even_if_its_the_same_closure() { $closure = function(){}; $this->shouldThrow()->duringAssertEquals($closure, $closure); } } shouldHaveType('SebastianBergmann\Comparator\Factory'); } function it_should_have_ClosureComparator_registered() { $comparator = $this->getInstance()->getComparatorFor(function(){}, function(){}); $comparator->shouldHaveType('Prophecy\Comparator\ClosureComparator'); } } shouldHaveType('SebastianBergmann\Comparator\ObjectComparator'); } function it_accepts_only_prophecy_objects() { $this->accepts(123, 321)->shouldReturn(false); $this->accepts('string', 'string')->shouldReturn(false); $this->accepts(false, true)->shouldReturn(false); $this->accepts(true, false)->shouldReturn(false); $this->accepts((object)array(), (object)array())->shouldReturn(false); $this->accepts(function(){}, (object)array())->shouldReturn(false); $this->accepts(function(){}, function(){})->shouldReturn(false); $prophet = new Prophet(); $prophecy = $prophet->prophesize('Prophecy\Prophecy\ObjectProphecy'); $this->accepts($prophecy, $prophecy)->shouldReturn(true); } function it_asserts_that_an_object_is_equal_to_its_revealed_prophecy() { $prophet = new Prophet(); $prophecy = $prophet->prophesize('Prophecy\Prophecy\ObjectProphecy'); $this->shouldNotThrow()->duringAssertEquals($prophecy->reveal(), $prophecy); } } shouldBeAnInstanceOf('Prophecy\Doubler\ClassPatch\ClassPatchInterface'); } function its_priority_is_100() { $this->getPriority()->shouldReturn(100); } function it_supports_anything(ClassNode $node) { $this->supports($node)->shouldReturn(true); } function it_makes_all_constructor_arguments_optional( ClassNode $class, MethodNode $method, ArgumentNode $arg1, ArgumentNode $arg2 ) { $class->hasMethod('__construct')->willReturn(true); $class->getMethod('__construct')->willReturn($method); $method->getArguments()->willReturn(array($arg1, $arg2)); $arg1->setDefault(null)->shouldBeCalled(); $arg2->setDefault(null)->shouldBeCalled(); $method->setCode(Argument::type('string'))->shouldBeCalled(); $this->apply($class); } function it_creates_new_constructor_if_object_has_none(ClassNode $class) { $class->hasMethod('__construct')->willReturn(false); $class->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode')) ->shouldBeCalled(); $this->apply($class); } } shouldBeAnInstanceOf('Prophecy\Doubler\ClassPatch\ClassPatchInterface'); } function its_priority_is_minus_50() { $this->getPriority()->shouldReturn(-50); } function it_uses_parent_code_for_setTraceOptions(ClassNode $node, MethodNode $method, MethodNode $getterMethod) { $node->hasMethod('setTraceOptions')->willReturn(true); $node->getMethod('setTraceOptions')->willReturn($method); $node->hasMethod('getTraceOptions')->willReturn(true); $node->getMethod('getTraceOptions')->willReturn($getterMethod); $method->useParentCode()->shouldBeCalled(); $getterMethod->useParentCode()->shouldBeCalled(); $this->apply($node); } } shouldBeAnInstanceOf('Prophecy\Doubler\ClassPatch\ClassPatchInterface'); } function its_priority_is_49() { $this->getPriority()->shouldReturn(49); } function it_will_remove_echo_and_eval_methods( ClassNode $node, MethodNode $method1, MethodNode $method2, MethodNode $method3 ) { $node->removeMethod('eval')->shouldBeCalled(); $node->removeMethod('echo')->shouldBeCalled(); $method1->getName()->willReturn('echo'); $method2->getName()->willReturn('eval'); $method3->getName()->willReturn('notKeyword'); $node->getMethods()->willReturn(array( 'echo' => $method1, 'eval' => $method2, 'notKeyword' => $method3, )); $this->apply($node); } } shouldBeAnInstanceOf('Prophecy\Doubler\ClassPatch\ClassPatchInterface'); } function it_supports_anything(ClassNode $node) { $this->supports($node)->shouldReturn(true); } function it_discovers_api_using_phpdoc(ClassNode $node) { $node->getParentClass()->willReturn('spec\Prophecy\Doubler\ClassPatch\MagicalApi'); $node->getInterfaces()->willReturn(array()); $node->addMethod(new MethodNode('undefinedMethod'))->shouldBeCalled(); $this->apply($node); } function it_ignores_existing_methods(ClassNode $node) { $node->getParentClass()->willReturn('spec\Prophecy\Doubler\ClassPatch\MagicalApiExtended'); $node->getInterfaces()->willReturn(array()); $node->addMethod(new MethodNode('undefinedMethod'))->shouldBeCalled(); $node->addMethod(new MethodNode('definedMethod'))->shouldNotBeCalled(); $this->apply($node); } function it_ignores_empty_methods_from_phpdoc(ClassNode $node) { $node->getParentClass()->willReturn('spec\Prophecy\Doubler\ClassPatch\MagicalApiInvalidMethodDefinition'); $node->getInterfaces()->willReturn(array()); $node->addMethod(new MethodNode(''))->shouldNotBeCalled(); $this->apply($node); } function it_discovers_api_using_phpdoc_from_implemented_interfaces(ClassNode $node) { $node->getParentClass()->willReturn('spec\Prophecy\Doubler\ClassPatch\MagicalApiImplemented'); $node->getInterfaces()->willReturn(array()); $node->addMethod(new MethodNode('implementedMethod'))->shouldBeCalled(); $this->apply($node); } /** * @param \Prophecy\Doubler\Generator\Node\ClassNode $node */ function it_discovers_api_using_phpdoc_from_own_interfaces($node) { $node->getParentClass()->willReturn('stdClass'); $node->getInterfaces()->willReturn(array('spec\Prophecy\Doubler\ClassPatch\MagicalApiImplemented')); $node->addMethod(new MethodNode('implementedMethod'))->shouldBeCalled(); $this->apply($node); } /** * @param \Prophecy\Doubler\Generator\Node\ClassNode $node */ function it_discovers_api_using_phpdoc_from_extended_parent_interfaces($node) { $node->getParentClass()->willReturn('spec\Prophecy\Doubler\ClassPatch\MagicalApiImplementedExtended'); $node->getInterfaces()->willReturn(array()); $node->addMethod(new MethodNode('implementedMethod'))->shouldBeCalled(); $this->apply($node); } function it_has_50_priority() { $this->getPriority()->shouldReturn(50); } } /** * @method void undefinedMethod() */ class MagicalApi { /** * @return void */ public function definedMethod() { } } /** * @method void invalidMethodDefinition * @method void * @method */ class MagicalApiInvalidMethodDefinition { } /** * @method void undefinedMethod() * @method void definedMethod() */ class MagicalApiExtended extends MagicalApi { } /** */ class MagicalApiImplemented implements MagicalApiInterface { } /** */ class MagicalApiImplementedExtended extends MagicalApiImplemented { } /** * @method void implementedMethod() */ interface MagicalApiInterface { } shouldBeAnInstanceOf('Prophecy\Doubler\ClassPatch\ClassPatchInterface'); } function it_has_priority_of_0() { $this->getPriority()->shouldReturn(0); } function it_supports_any_class(ClassNode $node) { $this->supports($node)->shouldReturn(true); } function it_forces_class_to_implement_ProphecySubjectInterface(ClassNode $node) { $node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface')->shouldBeCalled(); $node->addProperty('objectProphecy', 'private')->willReturn(null); $node->getMethods()->willReturn(array()); $node->hasMethod(Argument::any())->willReturn(false); $node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'))->willReturn(null); $node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'))->willReturn(null); $this->apply($node); } function it_forces_all_class_methods_except_constructor_to_proxy_calls_into_prophecy_makeCall( ClassNode $node, MethodNode $constructor, MethodNode $method1, MethodNode $method2, MethodNode $method3 ) { $node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface')->willReturn(null); $node->addProperty('objectProphecy', 'private')->willReturn(null); $node->hasMethod(Argument::any())->willReturn(false); $node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'))->willReturn(null); $node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'))->willReturn(null); $constructor->getName()->willReturn('__construct'); $method1->getName()->willReturn('method1'); $method2->getName()->willReturn('method2'); $method3->getName()->willReturn('method3'); $node->getMethods()->willReturn(array( 'method1' => $method1, 'method2' => $method2, 'method3' => $method3, )); $constructor->setCode(Argument::any())->shouldNotBeCalled(); $method1->setCode('return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());') ->shouldBeCalled(); $method2->setCode('return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());') ->shouldBeCalled(); $method3->setCode('return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());') ->shouldBeCalled(); $this->apply($node); } } shouldBeAnInstanceOf('Prophecy\Doubler\ClassPatch\ClassPatchInterface'); } function its_priority_is_50() { $this->getPriority()->shouldReturn(50); } function it_supports_ReflectionClass_only(ClassNode $reflectionClassNode, ClassNode $anotherClassNode) { $reflectionClassNode->getParentClass()->willReturn('ReflectionClass'); $anotherClassNode->getParentClass()->willReturn('stdClass'); $this->supports($reflectionClassNode)->shouldReturn(true); $this->supports($anotherClassNode)->shouldReturn(false); } function it_makes_all_newInstance_arguments_optional( ClassNode $class, MethodNode $method, ArgumentNode $arg1 ) { $class->getMethod('newInstance')->willReturn($method); $method->getArguments()->willReturn(array($arg1)); $arg1->setDefault(null)->shouldBeCalled(); $this->apply($class); } } shouldBeAnInstanceOf('Prophecy\Doubler\ClassPatch\ClassPatchInterface'); } function its_priority_is_50() { $this->getPriority()->shouldReturn(50); } function it_does_not_support_nodes_without_parent_class(ClassNode $node) { $node->getParentClass()->willReturn('stdClass'); $this->supports($node)->shouldReturn(false); } function it_supports_nodes_with_SplFileInfo_as_parent_class(ClassNode $node) { $node->getParentClass()->willReturn('SplFileInfo'); $this->supports($node)->shouldReturn(true); } function it_supports_nodes_with_derivative_of_SplFileInfo_as_parent_class(ClassNode $node) { $node->getParentClass()->willReturn('SplFileInfo'); $this->supports($node)->shouldReturn(true); } function it_adds_a_method_to_node_if_not_exists(ClassNode $node) { $node->hasMethod('__construct')->willReturn(false); $node->addMethod(Argument::any())->shouldBeCalled(); $node->getParentClass()->shouldBeCalled(); $this->apply($node); } function it_updates_existing_method_if_found(ClassNode $node, MethodNode $method) { $node->hasMethod('__construct')->willReturn(true); $node->getMethod('__construct')->willReturn($method); $node->getParentClass()->shouldBeCalled(); $method->useParentCode()->shouldBeCalled(); $this->apply($node); } function it_should_not_supply_a_file_for_a_directory_iterator(ClassNode $node, MethodNode $method) { $node->hasMethod('__construct')->willReturn(true); $node->getMethod('__construct')->willReturn($method); $node->getParentClass()->willReturn('DirectoryIterator'); $method->setCode(Argument::that(function($value) { return strpos($value, '.php') === false; }))->shouldBeCalled(); $this->apply($node); } function it_should_supply_a_file_for_a_spl_file_object(ClassNode $node, MethodNode $method) { $node->hasMethod('__construct')->willReturn(true); $node->getMethod('__construct')->willReturn($method); $node->getParentClass()->willReturn('SplFileObject'); $method->setCode(Argument::that(function($value) { return strpos($value, '.php') !== false; }))->shouldBeCalled(); $this->apply($node); } } shouldBeAnInstanceOf('Prophecy\Doubler\ClassPatch\ClassPatchInterface'); } function it_supports_class_that_implements_only_Traversable(ClassNode $node) { $node->getInterfaces()->willReturn(array('Traversable')); $this->supports($node)->shouldReturn(true); } function it_does_not_support_class_that_implements_Iterator(ClassNode $node) { $node->getInterfaces()->willReturn(array('Traversable', 'Iterator')); $this->supports($node)->shouldReturn(false); } function it_does_not_support_class_that_implements_IteratorAggregate(ClassNode $node) { $node->getInterfaces()->willReturn(array('Traversable', 'IteratorAggregate')); $this->supports($node)->shouldReturn(false); } function it_has_100_priority() { $this->getPriority()->shouldReturn(100); } function it_forces_node_to_implement_IteratorAggregate(ClassNode $node) { $node->addInterface('Iterator')->shouldBeCalled(); $node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'))->willReturn(null); $this->apply($node); } } beConstructedWith($mirror, $creator, $namer); } function it_does_not_have_patches_by_default() { $this->getClassPatches()->shouldHaveCount(0); } function its_registerClassPatch_adds_a_patch_to_the_doubler(ClassPatchInterface $patch) { $this->registerClassPatch($patch); $this->getClassPatches()->shouldReturn(array($patch)); } function its_getClassPatches_sorts_patches_by_priority( ClassPatchInterface $alt1, ClassPatchInterface $alt2, ClassPatchInterface $alt3, ClassPatchInterface $alt4 ) { $alt1->getPriority()->willReturn(2); $alt2->getPriority()->willReturn(50); $alt3->getPriority()->willReturn(10); $alt4->getPriority()->willReturn(0); $this->registerClassPatch($alt1); $this->registerClassPatch($alt2); $this->registerClassPatch($alt3); $this->registerClassPatch($alt4); $this->getClassPatches()->shouldReturn(array($alt2, $alt3, $alt1, $alt4)); } function its_double_mirrors_alterates_and_instantiates_provided_class( $mirror, $creator, $namer, ClassPatchInterface $alt1, ClassPatchInterface $alt2, \ReflectionClass $class, \ReflectionClass $interface1, \ReflectionClass $interface2, ClassNode $node ) { $mirror->reflect($class, array($interface1, $interface2))->willReturn($node); $alt1->supports($node)->willReturn(true); $alt2->supports($node)->willReturn(false); $alt1->getPriority()->willReturn(1); $alt2->getPriority()->willReturn(2); $namer->name($class, array($interface1, $interface2))->willReturn('SplStack'); $class->getName()->willReturn('stdClass'); $interface1->getName()->willReturn('ArrayAccess'); $interface2->getName()->willReturn('Iterator'); $alt1->apply($node)->shouldBeCalled(); $alt2->apply($node)->shouldNotBeCalled(); $creator->create('SplStack', $node)->shouldBeCalled(); $this->registerClassPatch($alt1); $this->registerClassPatch($alt2); $this->double($class, array($interface1, $interface2)) ->shouldReturnAnInstanceOf('SplStack'); } function it_double_instantiates_a_class_with_constructor_argument( $mirror, \ReflectionClass $class, ClassNode $node, $namer ) { $class->getName()->willReturn('ReflectionClass'); $mirror->reflect($class, array())->willReturn($node); $namer->name($class, array())->willReturn('ReflectionClass'); $double = $this->double($class, array(), array('stdClass')); $double->shouldBeAnInstanceOf('ReflectionClass'); $double->getName()->shouldReturn('stdClass'); } function it_can_instantiate_class_with_final_constructor( $mirror, \ReflectionClass $class, ClassNode $node, $namer ) { $class->getName()->willReturn('spec\Prophecy\Doubler\WithFinalConstructor'); $mirror->reflect($class, array())->willReturn($node); $namer->name($class, array())->willReturn('spec\Prophecy\Doubler\WithFinalConstructor'); $double = $this->double($class, array()); $double->shouldBeAnInstanceOf('spec\Prophecy\Doubler\WithFinalConstructor'); } } class WithFinalConstructor { final public function __construct() {} } getParentClass()->willReturn('RuntimeException'); $class->getInterfaces()->willReturn(array( 'Prophecy\Doubler\Generator\MirroredInterface', 'ArrayAccess', 'ArrayIterator' )); $class->getProperties()->willReturn(array('name' => 'public', 'email' => 'private')); $class->getMethods()->willReturn(array($method1, $method2, $method3)); $method1->getName()->willReturn('getName'); $method1->getVisibility()->willReturn('public'); $method1->returnsReference()->willReturn(false); $method1->isStatic()->willReturn(true); $method1->getArguments()->willReturn(array($argument11, $argument12)); $method1->hasReturnType()->willReturn(true); $method1->getReturnType()->willReturn('string'); $method1->getCode()->willReturn('return $this->name;'); $method2->getName()->willReturn('getEmail'); $method2->getVisibility()->willReturn('protected'); $method2->returnsReference()->willReturn(false); $method2->isStatic()->willReturn(false); $method2->getArguments()->willReturn(array($argument21)); $method2->hasReturnType()->willReturn(false); $method2->getCode()->willReturn('return $this->email;'); $method3->getName()->willReturn('getRefValue'); $method3->getVisibility()->willReturn('public'); $method3->returnsReference()->willReturn(true); $method3->isStatic()->willReturn(false); $method3->getArguments()->willReturn(array($argument31)); $method3->hasReturnType()->willReturn(false); $method3->getCode()->willReturn('return $this->refValue;'); $argument11->getName()->willReturn('fullname'); $argument11->getTypeHint()->willReturn('array'); $argument11->isOptional()->willReturn(true); $argument11->getDefault()->willReturn(null); $argument11->isPassedByReference()->willReturn(false); $argument11->isVariadic()->willReturn(false); $argument12->getName()->willReturn('class'); $argument12->getTypeHint()->willReturn('ReflectionClass'); $argument12->isOptional()->willReturn(false); $argument12->isPassedByReference()->willReturn(false); $argument12->isVariadic()->willReturn(false); $argument21->getName()->willReturn('default'); $argument21->getTypeHint()->willReturn('string'); $argument21->isOptional()->willReturn(true); $argument21->getDefault()->willReturn('ever.zet@gmail.com'); $argument21->isPassedByReference()->willReturn(false); $argument21->isVariadic()->willReturn(false); $argument31->getName()->willReturn('refValue'); $argument31->getTypeHint()->willReturn(null); $argument31->isOptional()->willReturn(false); $argument31->getDefault()->willReturn(); $argument31->isPassedByReference()->willReturn(false); $argument31->isVariadic()->willReturn(false); $code = $this->generate('CustomClass', $class); if (version_compare(PHP_VERSION, '7.0', '>=')) { $expected = <<<'PHP' namespace { class CustomClass extends \RuntimeException implements \Prophecy\Doubler\Generator\MirroredInterface, \ArrayAccess, \ArrayIterator { public $name; private $email; public static function getName(array $fullname = NULL, \ReflectionClass $class): string { return $this->name; } protected function getEmail(string $default = 'ever.zet@gmail.com') { return $this->email; } public function &getRefValue( $refValue) { return $this->refValue; } } } PHP; } else { $expected = <<<'PHP' namespace { class CustomClass extends \RuntimeException implements \Prophecy\Doubler\Generator\MirroredInterface, \ArrayAccess, \ArrayIterator { public $name; private $email; public static function getName(array $fullname = NULL, \ReflectionClass $class) { return $this->name; } protected function getEmail(\string $default = 'ever.zet@gmail.com') { return $this->email; } public function &getRefValue( $refValue) { return $this->refValue; } } } PHP; } $expected = strtr($expected, array("\r\n" => "\n", "\r" => "\n")); $code->shouldBe($expected); } function it_generates_proper_php_code_for_variadics( ClassNode $class, MethodNode $method1, MethodNode $method2, MethodNode $method3, MethodNode $method4, ArgumentNode $argument1, ArgumentNode $argument2, ArgumentNode $argument3, ArgumentNode $argument4 ) { $class->getParentClass()->willReturn('stdClass'); $class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface')); $class->getProperties()->willReturn(array()); $class->getMethods()->willReturn(array( $method1, $method2, $method3, $method4 )); $method1->getName()->willReturn('variadic'); $method1->getVisibility()->willReturn('public'); $method1->returnsReference()->willReturn(false); $method1->isStatic()->willReturn(false); $method1->getArguments()->willReturn(array($argument1)); $method1->hasReturnType()->willReturn(false); $method1->getCode()->willReturn(''); $method2->getName()->willReturn('variadicByRef'); $method2->getVisibility()->willReturn('public'); $method2->returnsReference()->willReturn(false); $method2->isStatic()->willReturn(false); $method2->getArguments()->willReturn(array($argument2)); $method2->hasReturnType()->willReturn(false); $method2->getCode()->willReturn(''); $method3->getName()->willReturn('variadicWithType'); $method3->getVisibility()->willReturn('public'); $method3->returnsReference()->willReturn(false); $method3->isStatic()->willReturn(false); $method3->getArguments()->willReturn(array($argument3)); $method3->hasReturnType()->willReturn(false); $method3->getCode()->willReturn(''); $method4->getName()->willReturn('variadicWithTypeByRef'); $method4->getVisibility()->willReturn('public'); $method4->returnsReference()->willReturn(false); $method4->isStatic()->willReturn(false); $method4->getArguments()->willReturn(array($argument4)); $method4->hasReturnType()->willReturn(false); $method4->getCode()->willReturn(''); $argument1->getName()->willReturn('args'); $argument1->getTypeHint()->willReturn(null); $argument1->isOptional()->willReturn(false); $argument1->isPassedByReference()->willReturn(false); $argument1->isVariadic()->willReturn(true); $argument2->getName()->willReturn('args'); $argument2->getTypeHint()->willReturn(null); $argument2->isOptional()->willReturn(false); $argument2->isPassedByReference()->willReturn(true); $argument2->isVariadic()->willReturn(true); $argument3->getName()->willReturn('args'); $argument3->getTypeHint()->willReturn('\ReflectionClass'); $argument3->isOptional()->willReturn(false); $argument3->isPassedByReference()->willReturn(false); $argument3->isVariadic()->willReturn(true); $argument4->getName()->willReturn('args'); $argument4->getTypeHint()->willReturn('\ReflectionClass'); $argument4->isOptional()->willReturn(false); $argument4->isPassedByReference()->willReturn(true); $argument4->isVariadic()->willReturn(true); $code = $this->generate('CustomClass', $class); $expected = <<<'PHP' namespace { class CustomClass extends \stdClass implements \Prophecy\Doubler\Generator\MirroredInterface { public function variadic( ...$args) { } public function variadicByRef( &...$args) { } public function variadicWithType(\\ReflectionClass ...$args) { } public function variadicWithTypeByRef(\\ReflectionClass &...$args) { } } } PHP; $expected = strtr($expected, array("\r\n" => "\n", "\r" => "\n")); $code->shouldBe($expected); } function it_overrides_properly_methods_with_args_passed_by_reference( ClassNode $class, MethodNode $method, ArgumentNode $argument ) { $class->getParentClass()->willReturn('RuntimeException'); $class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface')); $class->getProperties()->willReturn(array()); $class->getMethods()->willReturn(array($method)); $method->getName()->willReturn('getName'); $method->getVisibility()->willReturn('public'); $method->isStatic()->willReturn(false); $method->getArguments()->willReturn(array($argument)); $method->hasReturnType()->willReturn(false); $method->returnsReference()->willReturn(false); $method->getCode()->willReturn('return $this->name;'); $argument->getName()->willReturn('fullname'); $argument->getTypeHint()->willReturn('array'); $argument->isOptional()->willReturn(true); $argument->getDefault()->willReturn(null); $argument->isPassedByReference()->willReturn(true); $argument->isVariadic()->willReturn(false); $code = $this->generate('CustomClass', $class); $expected =<<<'PHP' namespace { class CustomClass extends \RuntimeException implements \Prophecy\Doubler\Generator\MirroredInterface { public function getName(array &$fullname = NULL) { return $this->name; } } } PHP; $expected = strtr($expected, array("\r\n" => "\n", "\r" => "\n")); $code->shouldBe($expected); } function it_generates_empty_class_for_empty_ClassNode(ClassNode $class) { $class->getParentClass()->willReturn('stdClass'); $class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface')); $class->getProperties()->willReturn(array()); $class->getMethods()->willReturn(array()); $code = $this->generate('CustomClass', $class); $expected =<<<'PHP' namespace { class CustomClass extends \stdClass implements \Prophecy\Doubler\Generator\MirroredInterface { } } PHP; $expected = strtr($expected, array("\r\n" => "\n", "\r" => "\n")); $code->shouldBe($expected); } function it_wraps_class_in_namespace_if_it_is_namespaced(ClassNode $class) { $class->getParentClass()->willReturn('stdClass'); $class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface')); $class->getProperties()->willReturn(array()); $class->getMethods()->willReturn(array()); $code = $this->generate('My\Awesome\CustomClass', $class); $expected =<<<'PHP' namespace My\Awesome { class CustomClass extends \stdClass implements \Prophecy\Doubler\Generator\MirroredInterface { } } PHP; $expected = strtr($expected, array("\r\n" => "\n", "\r" => "\n")); $code->shouldBe($expected); } } beConstructedWith($generator); } function it_evaluates_code_generated_by_ClassCodeGenerator($generator, ClassNode $class) { $generator->generate('stdClass', $class)->shouldBeCalled()->willReturn( 'return 42;' ); $this->create('stdClass', $class)->shouldReturn(42); } function it_throws_an_exception_if_class_does_not_exist_after_evaluation($generator, ClassNode $class) { $generator->generate('CustomClass', $class)->shouldBeCalled()->willReturn( 'return 42;' ); $class->getParentClass()->willReturn('stdClass'); $class->getInterfaces()->willReturn(array('Interface1', 'Interface2')); $this->shouldThrow('Prophecy\Exception\Doubler\ClassCreatorException') ->duringCreate('CustomClass', $class); } } beConstructedWith('name'); } function it_is_not_be_passed_by_reference_by_default() { $this->shouldNotBePassedByReference(); } function it_is_passed_by_reference_if_marked() { $this->setAsPassedByReference(); $this->shouldBePassedByReference(); } function it_is_not_variadic_by_default() { $this->shouldNotBeVariadic(); } function it_is_variadic_if_marked() { $this->setAsVariadic(); $this->shouldBeVariadic(); } function it_does_not_have_default_by_default() { $this->shouldNotHaveDefault(); } function it_does_not_have_default_if_variadic() { $this->setDefault(null); $this->setAsVariadic(); $this->shouldNotHaveDefault(); } function it_does_have_default_if_not_variadic() { $this->setDefault(null); $this->setAsVariadic(false); $this->hasDefault()->shouldReturn(true); } function it_has_name_with_which_it_was_been_constructed() { $this->getName()->shouldReturn('name'); } function it_has_no_typehint_by_default() { $this->getTypeHint()->shouldReturn(null); } function its_typeHint_is_mutable() { $this->setTypeHint('array'); $this->getTypeHint()->shouldReturn('array'); } function it_does_not_have_default_value_by_default() { $this->getDefault()->shouldReturn(null); } function it_is_not_optional_by_default() { $this->isOptional()->shouldReturn(false); } function its_default_is_mutable() { $this->setDefault(array()); $this->getDefault()->shouldReturn(array()); } function it_is_marked_as_optional_when_default_is_set() { $this->setDefault(null); $this->isOptional()->shouldReturn(true); } } getParentClass()->shouldReturn('stdClass'); } function its_parentClass_is_mutable() { $this->setParentClass('Exception'); $this->getParentClass()->shouldReturn('Exception'); } function its_parentClass_is_set_to_stdClass_if_user_set_null() { $this->setParentClass(null); $this->getParentClass()->shouldReturn('stdClass'); } function it_does_not_implement_any_interface_by_default() { $this->getInterfaces()->shouldHaveCount(0); } function its_addInterface_adds_item_to_the_list_of_implemented_interfaces() { $this->addInterface('MyInterface'); $this->getInterfaces()->shouldHaveCount(1); } function its_hasInterface_returns_true_if_class_implements_interface() { $this->addInterface('MyInterface'); $this->hasInterface('MyInterface')->shouldReturn(true); } function its_hasInterface_returns_false_if_class_does_not_implements_interface() { $this->hasInterface('MyInterface')->shouldReturn(false); } function it_supports_implementation_of_multiple_interfaces() { $this->addInterface('MyInterface'); $this->addInterface('MySecondInterface'); $this->getInterfaces()->shouldHaveCount(2); } function it_ignores_same_interfaces_added_twice() { $this->addInterface('MyInterface'); $this->addInterface('MyInterface'); $this->getInterfaces()->shouldHaveCount(1); $this->getInterfaces()->shouldReturn(array('MyInterface')); } function it_does_not_have_methods_by_default() { $this->getMethods()->shouldHaveCount(0); } function it_can_has_methods(MethodNode $method1, MethodNode $method2) { $method1->getName()->willReturn('__construct'); $method2->getName()->willReturn('getName'); $this->addMethod($method1); $this->addMethod($method2); $this->getMethods()->shouldReturn(array( '__construct' => $method1, 'getName' => $method2 )); } function its_hasMethod_returns_true_if_method_exists(MethodNode $method) { $method->getName()->willReturn('getName'); $this->addMethod($method); $this->hasMethod('getName')->shouldReturn(true); } function its_getMethod_returns_method_by_name(MethodNode $method) { $method->getName()->willReturn('getName'); $this->addMethod($method); $this->getMethod('getName')->shouldReturn($method); } function its_hasMethod_returns_false_if_method_does_not_exists() { $this->hasMethod('getName')->shouldReturn(false); } function its_hasMethod_returns_false_if_method_has_been_removed(MethodNode $method) { $method->getName()->willReturn('getName'); $this->addMethod($method); $this->removeMethod('getName'); $this->hasMethod('getName')->shouldReturn(false); } function it_does_not_have_properties_by_default() { $this->getProperties()->shouldHaveCount(0); } function it_is_able_to_have_properties() { $this->addProperty('title'); $this->addProperty('text', 'private'); $this->getProperties()->shouldReturn(array( 'title' => 'public', 'text' => 'private' )); } function its_addProperty_does_not_accept_unsupported_visibility() { $this->shouldThrow('InvalidArgumentException')->duringAddProperty('title', 'town'); } function its_addProperty_lowercases_visibility_before_setting() { $this->addProperty('text', 'PRIVATE'); $this->getProperties()->shouldReturn(array('text' => 'private')); } function its_has_no_unextendable_methods_by_default() { $this->getUnextendableMethods()->shouldHaveCount(0); } function its_addUnextendableMethods_adds_an_unextendable_method() { $this->addUnextendableMethod('testMethod'); $this->getUnextendableMethods()->shouldHaveCount(1); } function its_methods_are_extendable_by_default() { $this->isExtendable('testMethod')->shouldReturn(true); } function its_unextendable_methods_are_not_extendable() { $this->addUnextendableMethod('testMethod'); $this->isExtendable('testMethod')->shouldReturn(false); } function its_addUnextendableMethods_doesnt_create_duplicates() { $this->addUnextendableMethod('testMethod'); $this->addUnextendableMethod('testMethod'); $this->getUnextendableMethods()->shouldHaveCount(1); } function it_throws_an_exception_when_adding_a_method_that_isnt_extendable(MethodNode $method) { $this->addUnextendableMethod('testMethod'); $method->getName()->willReturn('testMethod'); $expectedException = new MethodNotExtendableException( "Method `testMethod` is not extendable, so can not be added.", "stdClass", "testMethod" ); $this->shouldThrow($expectedException)->duringAddMethod($method); } } beConstructedWith('getTitle'); } function it_has_a_name() { $this->getName()->shouldReturn('getTitle'); } function it_has_public_visibility_by_default() { $this->getVisibility()->shouldReturn('public'); } function its_visibility_is_mutable() { $this->setVisibility('private'); $this->getVisibility()->shouldReturn('private'); } function it_is_not_static_by_default() { $this->shouldNotBeStatic(); } function it_does_not_return_a_reference_by_default() { $this->returnsReference()->shouldReturn(false); } function it_should_be_settable_as_returning_a_reference_through_setter() { $this->setReturnsReference(); $this->returnsReference()->shouldReturn(true); } function it_should_be_settable_as_static_through_setter() { $this->setStatic(); $this->shouldBeStatic(); } function it_accepts_only_supported_visibilities() { $this->shouldThrow('InvalidArgumentException')->duringSetVisibility('stealth'); } function it_lowercases_visibility_before_setting_it() { $this->setVisibility('Public'); $this->getVisibility()->shouldReturn('public'); } function its_useParentCode_causes_method_to_call_parent(ArgumentNode $argument1, ArgumentNode $argument2) { $argument1->getName()->willReturn('objectName'); $argument2->getName()->willReturn('default'); $argument1->isVariadic()->willReturn(false); $argument2->isVariadic()->willReturn(true); $this->addArgument($argument1); $this->addArgument($argument2); $this->useParentCode(); $this->getCode()->shouldReturn( 'return parent::getTitle($objectName, ...$default);' ); } function its_code_is_mutable() { $this->setCode('echo "code";'); $this->getCode()->shouldReturn('echo "code";'); } function its_reference_returning_methods_will_generate_exceptions() { $this->setCode('echo "code";'); $this->setReturnsReference(); $this->getCode()->shouldReturn("throw new \Prophecy\Exception\Doubler\ReturnByReferenceException('Returning by reference not supported', get_class(\$this), 'getTitle');"); } function its_setCode_provided_with_null_cleans_method_body() { $this->setCode(null); $this->getCode()->shouldReturn(''); } function it_is_constructable_with_code() { $this->beConstructedWith('getTitle', 'die();'); $this->getCode()->shouldReturn('die();'); } function it_does_not_have_arguments_by_default() { $this->getArguments()->shouldHaveCount(0); } function it_supports_adding_arguments(ArgumentNode $argument1, ArgumentNode $argument2) { $this->addArgument($argument1); $this->addArgument($argument2); $this->getArguments()->shouldReturn(array($argument1, $argument2)); } function it_does_not_have_return_type_by_default() { $this->hasReturnType()->shouldReturn(false); } function it_setReturnType_sets_return_type() { $returnType = 'string'; $this->setReturnType($returnType); $this->hasReturnType()->shouldReturn(true); $this->getReturnType()->shouldReturn($returnType); } } beConstructedWith($doubler); } function it_returns_anonymous_double_instance_by_default($doubler, ProphecySubjectInterface $double) { $doubler->double(null, array())->willReturn($double); $this->getInstance()->shouldReturn($double); } function it_returns_class_double_instance_if_set($doubler, ProphecySubjectInterface $double, \ReflectionClass $class) { $doubler->double($class, array())->willReturn($double); $this->setParentClass($class); $this->getInstance()->shouldReturn($double); } function it_returns_same_double_instance_if_called_2_times( $doubler, ProphecySubjectInterface $double1, ProphecySubjectInterface $double2 ) { $doubler->double(null, array())->willReturn($double1); $doubler->double(null, array())->willReturn($double2); $this->getInstance()->shouldReturn($double2); $this->getInstance()->shouldReturn($double2); } function its_setParentClass_throws_ClassNotFoundException_if_class_not_found() { $this->shouldThrow('Prophecy\Exception\Doubler\ClassNotFoundException') ->duringSetParentClass('SomeUnexistingClass'); } function its_setParentClass_throws_exception_if_prophecy_is_already_created( $doubler, ProphecySubjectInterface $double ) { $doubler->double(null, array())->willReturn($double); $this->getInstance(); $this->shouldThrow('Prophecy\Exception\Doubler\DoubleException') ->duringSetParentClass('stdClass'); } function its_addInterface_throws_InterfaceNotFoundException_if_no_interface_found() { $this->shouldThrow('Prophecy\Exception\Doubler\InterfaceNotFoundException') ->duringAddInterface('SomeUnexistingInterface'); } function its_addInterface_throws_exception_if_prophecy_is_already_created( $doubler, ProphecySubjectInterface $double ) { $doubler->double(null, array())->willReturn($double); $this->getInstance(); $this->shouldThrow('Prophecy\Exception\Doubler\DoubleException') ->duringAddInterface('ArrayAccess'); } } getName()->willReturn('stdClass'); $this->name($class, array())->shouldStartWith('Double\stdClass\\'); } function its_name_generates_name_based_on_namespaced_class_reflection(\ReflectionClass $class) { $class->getName()->willReturn('Some\Custom\Class'); $this->name($class, array())->shouldStartWith('Double\Some\Custom\Class\P'); } function its_name_generates_name_based_on_interface_shortnames( \ReflectionClass $interface1, \ReflectionClass $interface2 ) { $interface1->getShortName()->willReturn('HandlerInterface'); $interface2->getShortName()->willReturn('LoaderInterface'); $this->name(null, array($interface1, $interface2))->shouldStartWith( 'Double\HandlerInterface\LoaderInterface\P' ); } function it_generates_proper_name_for_no_class_and_interfaces_list() { $this->name(null, array())->shouldStartWith('Double\stdClass\P'); } function its_name_generates_name_based_only_on_class_if_its_available( \ReflectionClass $class, \ReflectionClass $interface1, \ReflectionClass $interface2 ) { $class->getName()->willReturn('Some\Custom\Class'); $interface1->getShortName()->willReturn('HandlerInterface'); $interface2->getShortName()->willReturn('LoaderInterface'); $this->name($class, array($interface1, $interface2))->shouldStartWith( 'Double\Some\Custom\Class\P' ); } public function getMatchers() { return array( 'startWith' => function ($subject, $string) { return 0 === strpos($subject, $string); }, ); } } beConstructedWith('msg', $objectProphecy, 'getName', array('arg1', 'arg2')); } function it_is_prophecy_exception() { $this->shouldBeAnInstanceOf('Prophecy\Exception\Prophecy\ObjectProphecyException'); } function it_exposes_method_name_through_getter() { $this->getMethodName()->shouldReturn('getName'); } function it_exposes_arguments_through_getter() { $this->getArguments()->shouldReturn(array('arg1', 'arg2')); } } beConstructedWith('', $node); } function it_is_a_prophecy_exception() { $this->shouldBeAnInstanceOf('Prophecy\Exception\Exception'); $this->shouldBeAnInstanceOf('Prophecy\Exception\Doubler\DoublerException'); } function it_contains_a_reflected_node($node) { $this->getClassNode()->shouldReturn($node); } } beConstructedWith('', $class); } function it_is_a_prophecy_exception() { $this->shouldBeAnInstanceOf('Prophecy\Exception\Exception'); $this->shouldBeAnInstanceOf('Prophecy\Exception\Doubler\DoublerException'); } function it_contains_a_reflected_class_link($class) { $this->getReflectedClass()->shouldReturn($class); } } beConstructedWith('msg', 'CustomClass'); } function it_is_a_prophecy_exception() { $this->shouldBeAnInstanceOf('Prophecy\Exception\Exception'); $this->shouldBeAnInstanceOf('Prophecy\Exception\Doubler\DoubleException'); } function its_getClassname_returns_classname() { $this->getClassname()->shouldReturn('CustomClass'); } } shouldBeAnInstanceOf('RuntimeException'); $this->shouldBeAnInstanceOf('Prophecy\Exception\Doubler\DoublerException'); } } beConstructedWith('msg', 'CustomInterface'); } function it_extends_ClassNotFoundException() { $this->shouldBeAnInstanceOf('Prophecy\Exception\Doubler\ClassNotFoundException'); } function its_getClassname_returns_classname() { $this->getClassname()->shouldReturn('CustomInterface'); } } beConstructedWith('', 'User', 'getName'); } function it_is_DoubleException() { $this->shouldHaveType('Prophecy\Exception\Doubler\DoubleException'); } function it_has_MethodName() { $this->getMethodName()->shouldReturn('getName'); } function it_has_classname() { $this->getClassName()->shouldReturn('User'); } } beConstructedWith('', 'User', 'getName', array(1, 2, 3)); } function it_is_DoubleException() { $this->shouldHaveType('Prophecy\Exception\Doubler\DoubleException'); } function it_has_MethodName() { $this->getMethodName()->shouldReturn('getName'); } function it_has_classnamej() { $this->getClassname()->shouldReturn('User'); } function it_has_an_arguments_list() { $this->getArguments()->shouldReturn(array(1, 2, 3)); } function it_has_a_default_null_argument_list() { $this->beConstructedWith('', 'User', 'getName'); $this->getArguments()->shouldReturn(null); } } beConstructedWith(null); } function it_is_prediction_exception() { $this->shouldBeAnInstanceOf('RuntimeException'); $this->shouldBeAnInstanceOf('Prophecy\Exception\Prediction\PredictionException'); } function it_can_store_objectProphecy_link(ObjectProphecy $object) { $this->setObjectProphecy($object); $this->getObjectProphecy()->shouldReturn($object); } function it_should_not_have_exceptions_at_the_beginning() { $this->getExceptions()->shouldHaveCount(0); } function it_should_append_exception_through_append_method(PredictionException $exception) { $exception->getMessage()->willReturn('Exception #1'); $this->append($exception); $this->getExceptions()->shouldReturn(array($exception)); } function it_should_update_message_during_append(PredictionException $exception) { $exception->getMessage()->willReturn('Exception #1'); $this->append($exception); $this->getMessage()->shouldReturn(" Exception #1"); } } getObjectProphecy()->willReturn($objectProphecy); $this->beConstructedWith('message', $methodProphecy); } function it_is_PredictionException() { $this->shouldHaveType('Prophecy\Exception\Prediction\PredictionException'); } function it_extends_MethodProphecyException() { $this->shouldHaveType('Prophecy\Exception\Prophecy\MethodProphecyException'); } } getObjectProphecy()->willReturn($objectProphecy); $this->beConstructedWith('message', $methodProphecy, 5, array($call1, $call2)); } function it_extends_UnexpectedCallsException() { $this->shouldBeAnInstanceOf('Prophecy\Exception\Prediction\UnexpectedCallsException'); } function it_should_expose_expectedCount_through_getter() { $this->getExpectedCount()->shouldReturn(5); } } getObjectProphecy()->willReturn($objectProphecy); $this->beConstructedWith('message', $methodProphecy, array($call1, $call2)); } function it_is_PredictionException() { $this->shouldHaveType('Prophecy\Exception\Prediction\PredictionException'); } function it_extends_MethodProphecyException() { $this->shouldHaveType('Prophecy\Exception\Prophecy\MethodProphecyException'); } function it_should_expose_calls_list_through_getter($call1, $call2) { $this->getCalls()->shouldReturn(array($call1, $call2)); } } getObjectProphecy()->willReturn($objectProphecy); $this->beConstructedWith('message', $methodProphecy); } function it_extends_DoubleException() { $this->shouldBeAnInstanceOf('Prophecy\Exception\Prophecy\ObjectProphecyException'); } function it_holds_a_stub_reference($methodProphecy) { $this->getMethodProphecy()->shouldReturn($methodProphecy); } } beConstructedWith('message', $objectProphecy); } function it_should_be_a_prophecy_exception() { $this->shouldBeAnInstanceOf('Prophecy\Exception\Prophecy\ProphecyException'); } function it_holds_double_reference($objectProphecy) { $this->getObjectProphecy()->shouldReturn($objectProphecy); } } beConstructedWith('get_class'); } function it_is_prediction() { $this->shouldHaveType('Prophecy\Prediction\PredictionInterface'); } function it_proxies_call_to_callback(ObjectProphecy $object, MethodProphecy $method, Call $call) { $returnFirstCallCallback = function ($calls, $object, $method) { throw new RuntimeException; }; $this->beConstructedWith($returnFirstCallCallback); $this->shouldThrow('RuntimeException')->duringCheck(array($call), $object, $method); } } shouldHaveType('Prophecy\Prediction\PredictionInterface'); } function it_does_nothing_if_there_is_more_than_one_call_been_made( ObjectProphecy $object, MethodProphecy $method, Call $call ) { $this->check(array($call), $object, $method)->shouldReturn(null); } function it_throws_NoCallsException_if_no_calls_found( ObjectProphecy $object, MethodProphecy $method, ArgumentsWildcard $arguments ) { $method->getObjectProphecy()->willReturn($object); $method->getMethodName()->willReturn('getName'); $method->getArgumentsWildcard()->willReturn($arguments); $arguments->__toString()->willReturn('123'); $object->reveal()->willReturn(new \stdClass()); $object->findProphecyMethodCalls('getName', Argument::any())->willReturn(array()); $this->shouldThrow('Prophecy\Exception\Prediction\NoCallsException') ->duringCheck(array(), $object, $method); } } beConstructedWith(2); } function it_is_prediction() { $this->shouldHaveType('Prophecy\Prediction\PredictionInterface'); } function it_does_nothing_if_there_were_exact_amount_of_calls_being_made( ObjectProphecy $object, MethodProphecy $method, Call $call1, Call $call2 ) { $this->check(array($call1, $call2), $object, $method)->shouldReturn(null); } function it_throws_UnexpectedCallsCountException_if_calls_found( ObjectProphecy $object, MethodProphecy $method, Call $call, ArgumentsWildcard $arguments ) { $method->getObjectProphecy()->willReturn($object); $method->getMethodName()->willReturn('getName'); $method->getArgumentsWildcard()->willReturn($arguments); $arguments->__toString()->willReturn('123'); $call->getMethodName()->willReturn('getName'); $call->getArguments()->willReturn(array(5, 4, 'three')); $call->getCallPlace()->willReturn('unknown'); $this->shouldThrow('Prophecy\Exception\Prediction\UnexpectedCallsCountException') ->duringCheck(array($call), $object, $method); } } shouldHaveType('Prophecy\Prediction\PredictionInterface'); } function it_does_nothing_if_there_is_no_calls_made(ObjectProphecy $object, MethodProphecy $method) { $this->check(array(), $object, $method)->shouldReturn(null); } function it_throws_UnexpectedCallsException_if_calls_found( ObjectProphecy $object, MethodProphecy $method, Call $call, ArgumentsWildcard $arguments ) { $method->getObjectProphecy()->willReturn($object); $method->getMethodName()->willReturn('getName'); $method->getArgumentsWildcard()->willReturn($arguments); $arguments->__toString()->willReturn('123'); $call->getMethodName()->willReturn('getName'); $call->getArguments()->willReturn(array(5, 4, 'three')); $call->getCallPlace()->willReturn('unknown'); $this->shouldThrow('Prophecy\Exception\Prediction\UnexpectedCallsException') ->duringCheck(array($call), $object, $method); } } beConstructedWith('get_class'); } function it_is_promise() { $this->shouldBeAnInstanceOf('Prophecy\Promise\PromiseInterface'); } function it_should_execute_closure_callback(ObjectProphecy $object, MethodProphecy $method) { $firstArgumentCallback = function ($args) { return $args[0]; }; $this->beConstructedWith($firstArgumentCallback); $this->execute(array('one', 'two'), $object, $method)->shouldReturn('one'); } function it_should_execute_static_array_callback(ObjectProphecy $object, MethodProphecy $method) { $firstArgumentCallback = array('spec\Prophecy\Promise\ClassCallback', 'staticCallbackMethod'); $this->beConstructedWith($firstArgumentCallback); $this->execute(array('one', 'two'), $object, $method)->shouldReturn('one'); } function it_should_execute_instance_array_callback(ObjectProphecy $object, MethodProphecy $method) { $class = new ClassCallback(); $firstArgumentCallback = array($class, 'callbackMethod'); $this->beConstructedWith($firstArgumentCallback); $this->execute(array('one', 'two'), $object, $method)->shouldReturn('one'); } function it_should_execute_string_function_callback(ObjectProphecy $object, MethodProphecy $method) { $firstArgumentCallback = 'spec\Prophecy\Promise\functionCallbackFirstArgument'; $this->beConstructedWith($firstArgumentCallback); $this->execute(array('one', 'two'), $object, $method)->shouldReturn('one'); } } /** * Class used to test callbackpromise * * @param array * @return string */ class ClassCallback { /** * @param array $args */ function callbackMethod($args) { return $args[0]; } /** * @param array $args */ static function staticCallbackMethod($args) { return $args[0]; } } /** * Callback function used to test callbackpromise * * @param array * @return string */ function functionCallbackFirstArgument($args) { return $args[0]; } shouldBeAnInstanceOf('Prophecy\Promise\PromiseInterface'); } function it_should_return_first_argument_if_provided(ObjectProphecy $object, MethodProphecy $method) { $this->execute(array('one', 'two'), $object, $method)->shouldReturn('one'); } function it_should_return_null_if_no_arguments_provided(ObjectProphecy $object, MethodProphecy $method) { $this->execute(array(), $object, $method)->shouldReturn(null); } function it_should_return_nth_argument_if_provided(ObjectProphecy $object, MethodProphecy $method) { $this->beConstructedWith(1); $this->execute(array('one', 'two'), $object, $method)->shouldReturn('two'); } } beConstructedWith(array(42)); } function it_is_promise() { $this->shouldBeAnInstanceOf('Prophecy\Promise\PromiseInterface'); } function it_returns_value_it_was_constructed_with(ObjectProphecy $object, MethodProphecy $method) { $this->execute(array(), $object, $method)->shouldReturn(42); } function it_always_returns_last_value_left_in_the_return_values(ObjectProphecy $object, MethodProphecy $method) { $this->execute(array(), $object, $method)->shouldReturn(42); $this->execute(array(), $object, $method)->shouldReturn(42); } function it_consequently_returns_multiple_values_it_was_constructed_with( ObjectProphecy $object, MethodProphecy $method ) { $this->beConstructedWith(array(42, 24, 12)); $this->execute(array(), $object, $method)->shouldReturn(42); $this->execute(array(), $object, $method)->shouldReturn(24); $this->execute(array(), $object, $method)->shouldReturn(12); } function it_returns_null_if_constructed_with_empty_array(ObjectProphecy $object, MethodProphecy $method) { $this->beConstructedWith(array()); $this->execute(array(), $object, $method)->shouldReturn(null); } } beConstructedWith('RuntimeException'); } function it_is_promise() { $this->shouldBeAnInstanceOf('Prophecy\Promise\PromiseInterface'); } function it_instantiates_and_throws_exception_from_provided_classname(ObjectProphecy $object, MethodProphecy $method) { $this->beConstructedWith('InvalidArgumentException'); $this->shouldThrow('InvalidArgumentException') ->duringExecute(array(), $object, $method); } function it_instantiates_exceptions_with_required_arguments(ObjectProphecy $object, MethodProphecy $method) { $this->beConstructedWith('spec\Prophecy\Promise\RequiredArgumentException'); $this->shouldThrow('spec\Prophecy\Promise\RequiredArgumentException') ->duringExecute(array(), $object, $method); } function it_throws_provided_exception(ObjectProphecy $object, MethodProphecy $method) { $this->beConstructedWith($exc = new \RuntimeException('Some exception')); $this->shouldThrow($exc)->duringExecute(array(), $object, $method); } } class RequiredArgumentException extends \Exception { final public function __construct($message, $code) {} } reveal()->willReturn($reflection); $this->beConstructedWith($objectProphecy, 'getName', null); } function it_is_initializable() { $this->shouldHaveType('Prophecy\Prophecy\MethodProphecy'); } function its_constructor_throws_MethodNotFoundException_for_unexisting_method($objectProphecy) { $this->shouldThrow('Prophecy\Exception\Doubler\MethodNotFoundException')->during( '__construct', array($objectProphecy, 'getUnexisting', null) ); } function its_constructor_throws_MethodProphecyException_for_final_methods($objectProphecy, ClassWithFinalMethod $subject) { $objectProphecy->reveal()->willReturn($subject); $this->shouldThrow('Prophecy\Exception\Prophecy\MethodProphecyException')->during( '__construct', array($objectProphecy, 'finalMethod', null) ); } function its_constructor_transforms_array_passed_as_3rd_argument_to_ArgumentsWildcard( $objectProphecy ) { $this->beConstructedWith($objectProphecy, 'getName', array(42, 33)); $wildcard = $this->getArgumentsWildcard(); $wildcard->shouldNotBe(null); $wildcard->__toString()->shouldReturn('exact(42), exact(33)'); } function its_constructor_does_not_touch_third_argument_if_it_is_null($objectProphecy) { $this->beConstructedWith($objectProphecy, 'getName', null); $wildcard = $this->getArgumentsWildcard(); $wildcard->shouldBe(null); } function it_records_promise_through_will_method(PromiseInterface $promise, $objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $this->will($promise); $this->getPromise()->shouldReturn($promise); } function it_adds_itself_to_ObjectProphecy_during_call_to_will(PromiseInterface $objectProphecy, $promise) { $objectProphecy->addMethodProphecy($this)->shouldBeCalled(); $this->will($promise); } function it_adds_ReturnPromise_during_willReturn_call($objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $this->willReturn(42); $this->getPromise()->shouldBeAnInstanceOf('Prophecy\Promise\ReturnPromise'); } function it_adds_ThrowPromise_during_willThrow_call($objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $this->willThrow('RuntimeException'); $this->getPromise()->shouldBeAnInstanceOf('Prophecy\Promise\ThrowPromise'); } function it_adds_ReturnArgumentPromise_during_willReturnArgument_call($objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $this->willReturnArgument(); $this->getPromise()->shouldBeAnInstanceOf('Prophecy\Promise\ReturnArgumentPromise'); } function it_adds_ReturnArgumentPromise_during_willReturnArgument_call_with_index_argument($objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $this->willReturnArgument(1); $promise = $this->getPromise(); $promise->shouldBeAnInstanceOf('Prophecy\Promise\ReturnArgumentPromise'); $promise->execute(array('one', 'two'), $objectProphecy, $this)->shouldReturn('two'); } function it_adds_CallbackPromise_during_will_call_with_callback_argument($objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $callback = function () {}; $this->will($callback); $this->getPromise()->shouldBeAnInstanceOf('Prophecy\Promise\CallbackPromise'); } function it_records_prediction_through_should_method(PredictionInterface $prediction, $objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $this->callOnWrappedObject('should', array($prediction)); $this->getPrediction()->shouldReturn($prediction); } function it_adds_CallbackPrediction_during_should_call_with_callback_argument($objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $callback = function () {}; $this->callOnWrappedObject('should', array($callback)); $this->getPrediction()->shouldBeAnInstanceOf('Prophecy\Prediction\CallbackPrediction'); } function it_adds_itself_to_ObjectProphecy_during_call_to_should($objectProphecy, PredictionInterface $prediction) { $objectProphecy->addMethodProphecy($this)->shouldBeCalled(); $this->callOnWrappedObject('should', array($prediction)); } function it_adds_CallPrediction_during_shouldBeCalled_call($objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $this->callOnWrappedObject('shouldBeCalled', array()); $this->getPrediction()->shouldBeAnInstanceOf('Prophecy\Prediction\CallPrediction'); } function it_adds_NoCallsPrediction_during_shouldNotBeCalled_call($objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $this->callOnWrappedObject('shouldNotBeCalled', array()); $this->getPrediction()->shouldBeAnInstanceOf('Prophecy\Prediction\NoCallsPrediction'); } function it_adds_CallTimesPrediction_during_shouldBeCalledTimes_call($objectProphecy) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $this->callOnWrappedObject('shouldBeCalledTimes', array(5)); $this->getPrediction()->shouldBeAnInstanceOf('Prophecy\Prediction\CallTimesPrediction'); } function it_checks_prediction_via_shouldHave_method_call( $objectProphecy, ArgumentsWildcard $arguments, PredictionInterface $prediction, Call $call1, Call $call2 ) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $prediction->check(array($call1, $call2), $objectProphecy->getWrappedObject(), $this)->shouldBeCalled(); $objectProphecy->findProphecyMethodCalls('getName', $arguments)->willReturn(array($call1, $call2)); $this->withArguments($arguments); $this->callOnWrappedObject('shouldHave', array($prediction)); } function it_sets_return_promise_during_shouldHave_call_if_none_was_set_before( $objectProphecy, ArgumentsWildcard $arguments, PredictionInterface $prediction, Call $call1, Call $call2 ) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $prediction->check(array($call1, $call2), $objectProphecy->getWrappedObject(), $this)->shouldBeCalled(); $objectProphecy->findProphecyMethodCalls('getName', $arguments)->willReturn(array($call1, $call2)); $this->withArguments($arguments); $this->callOnWrappedObject('shouldHave', array($prediction)); $this->getPromise()->shouldReturnAnInstanceOf('Prophecy\Promise\ReturnPromise'); } function it_does_not_set_return_promise_during_shouldHave_call_if_it_was_set_before( $objectProphecy, ArgumentsWildcard $arguments, PredictionInterface $prediction, Call $call1, Call $call2, PromiseInterface $promise ) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $prediction->check(array($call1, $call2), $objectProphecy->getWrappedObject(), $this)->shouldBeCalled(); $objectProphecy->findProphecyMethodCalls('getName', $arguments)->willReturn(array($call1, $call2)); $this->will($promise); $this->withArguments($arguments); $this->callOnWrappedObject('shouldHave', array($prediction)); $this->getPromise()->shouldReturn($promise); } function it_records_checked_predictions( $objectProphecy, ArgumentsWildcard $arguments, PredictionInterface $prediction1, PredictionInterface $prediction2, Call $call1, Call $call2, PromiseInterface $promise ) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $prediction1->check(array($call1, $call2), $objectProphecy->getWrappedObject(), $this)->willReturn(); $prediction2->check(array($call1, $call2), $objectProphecy->getWrappedObject(), $this)->willReturn(); $objectProphecy->findProphecyMethodCalls('getName', $arguments)->willReturn(array($call1, $call2)); $this->will($promise); $this->withArguments($arguments); $this->callOnWrappedObject('shouldHave', array($prediction1)); $this->callOnWrappedObject('shouldHave', array($prediction2)); $this->getCheckedPredictions()->shouldReturn(array($prediction1, $prediction2)); } function it_records_even_failed_checked_predictions( $objectProphecy, ArgumentsWildcard $arguments, PredictionInterface $prediction, Call $call1, Call $call2, PromiseInterface $promise ) { $objectProphecy->addMethodProphecy($this)->willReturn(null); $prediction->check(array($call1, $call2), $objectProphecy->getWrappedObject(), $this)->willThrow(new \RuntimeException()); $objectProphecy->findProphecyMethodCalls('getName', $arguments)->willReturn(array($call1, $call2)); $this->will($promise); $this->withArguments($arguments); try { $this->callOnWrappedObject('shouldHave', array($prediction)); } catch (\Exception $e) {} $this->getCheckedPredictions()->shouldReturn(array($prediction)); } function it_checks_prediction_via_shouldHave_method_call_with_callback( $objectProphecy, ArgumentsWildcard $arguments, Call $call1, Call $call2 ) { $callback = function ($calls, $object, $method) { throw new \RuntimeException; }; $objectProphecy->findProphecyMethodCalls('getName', $arguments)->willReturn(array($call1, $call2)); $this->withArguments($arguments); $this->shouldThrow('RuntimeException')->duringShouldHave($callback); } function it_does_nothing_during_checkPrediction_if_no_prediction_set() { $this->checkPrediction()->shouldReturn(null); } function it_checks_set_prediction_during_checkPrediction( $objectProphecy, ArgumentsWildcard $arguments, PredictionInterface $prediction, Call $call1, Call $call2 ) { $prediction->check(array($call1, $call2), $objectProphecy->getWrappedObject(), $this)->shouldBeCalled(); $objectProphecy->findProphecyMethodCalls('getName', $arguments)->willReturn(array($call1, $call2)); $objectProphecy->addMethodProphecy($this)->willReturn(null); $this->withArguments($arguments); $this->callOnWrappedObject('should', array($prediction)); $this->checkPrediction(); } function it_links_back_to_ObjectProphecy_through_getter($objectProphecy) { $this->getObjectProphecy()->shouldReturn($objectProphecy); } function it_has_MethodName() { $this->getMethodName()->shouldReturn('getName'); } function it_contains_ArgumentsWildcard_it_was_constructed_with($objectProphecy, ArgumentsWildcard $wildcard) { $this->beConstructedWith($objectProphecy, 'getName', $wildcard); $this->getArgumentsWildcard()->shouldReturn($wildcard); } function its_ArgumentWildcard_is_mutable_through_setter(ArgumentsWildcard $wildcard) { $this->withArguments($wildcard); $this->getArgumentsWildcard()->shouldReturn($wildcard); } function its_withArguments_transforms_passed_array_into_ArgumentsWildcard() { $this->withArguments(array(42, 33)); $wildcard = $this->getArgumentsWildcard(); $wildcard->shouldNotBe(null); $wildcard->__toString()->shouldReturn('exact(42), exact(33)'); } function its_withArguments_throws_exception_if_wrong_arguments_provided() { $this->shouldThrow('Prophecy\Exception\InvalidArgumentException')->duringWithArguments(42); } } beConstructedWith($lazyDouble); $lazyDouble->getInstance()->willReturn($double); } function it_implements_ProphecyInterface() { $this->shouldBeAnInstanceOf('Prophecy\Prophecy\ProphecyInterface'); } function it_sets_parentClass_during_willExtend_call($lazyDouble) { $lazyDouble->setParentClass('123')->shouldBeCalled(); $this->willExtend('123'); } function it_adds_interface_during_willImplement_call($lazyDouble) { $lazyDouble->addInterface('222')->shouldBeCalled(); $this->willImplement('222'); } function it_sets_constructor_arguments_during_willBeConstructedWith_call($lazyDouble) { $lazyDouble->setArguments(array(1, 2, 5))->shouldBeCalled(); $this->willBeConstructedWith(array(1, 2, 5)); } function it_does_not_have_method_prophecies_by_default() { $this->getMethodProphecies()->shouldHaveCount(0); } function it_should_get_method_prophecies_by_method_name( MethodProphecy $method1, MethodProphecy $method2, ArgumentsWildcard $arguments ) { $method1->getMethodName()->willReturn('getName'); $method1->getArgumentsWildcard()->willReturn($arguments); $method2->getMethodName()->willReturn('setName'); $method2->getArgumentsWildcard()->willReturn($arguments); $this->addMethodProphecy($method1); $this->addMethodProphecy($method2); $methods = $this->getMethodProphecies('setName'); $methods->shouldHaveCount(1); $methods[0]->getMethodName()->shouldReturn('setName'); } function it_should_return_empty_array_if_no_method_prophecies_found() { $methods = $this->getMethodProphecies('setName'); $methods->shouldHaveCount(0); } function it_should_proxy_makeProphecyMethodCall_to_CallCenter($lazyDouble, CallCenter $callCenter) { $this->beConstructedWith($lazyDouble, $callCenter); $callCenter->makeCall($this->getWrappedObject(), 'setName', array('everzet'))->willReturn(42); $this->makeProphecyMethodCall('setName', array('everzet'))->shouldReturn(42); } function it_should_reveal_arguments_and_return_values_from_callCenter( $lazyDouble, CallCenter $callCenter, RevealerInterface $revealer ) { $this->beConstructedWith($lazyDouble, $callCenter, $revealer); $revealer->reveal(array('question'))->willReturn(array('life')); $revealer->reveal('answer')->willReturn(42); $callCenter->makeCall($this->getWrappedObject(), 'setName', array('life'))->willReturn('answer'); $this->makeProphecyMethodCall('setName', array('question'))->shouldReturn(42); } function it_should_proxy_getProphecyMethodCalls_to_CallCenter( $lazyDouble, CallCenter $callCenter, ArgumentsWildcard $wildcard, Call $call ) { $this->beConstructedWith($lazyDouble, $callCenter); $callCenter->findCalls('setName', $wildcard)->willReturn(array($call)); $this->findProphecyMethodCalls('setName', $wildcard)->shouldReturn(array($call)); } function its_addMethodProphecy_adds_method_prophecy( MethodProphecy $methodProphecy, ArgumentsWildcard $argumentsWildcard ) { $methodProphecy->getArgumentsWildcard()->willReturn($argumentsWildcard); $methodProphecy->getMethodName()->willReturn('getUsername'); $this->addMethodProphecy($methodProphecy); $this->getMethodProphecies()->shouldReturn(array( 'getUsername' => array($methodProphecy) )); } function its_addMethodProphecy_handles_prophecies_with_different_arguments( MethodProphecy $methodProphecy1, MethodProphecy $methodProphecy2, ArgumentsWildcard $argumentsWildcard1, ArgumentsWildcard $argumentsWildcard2 ) { $methodProphecy1->getArgumentsWildcard()->willReturn($argumentsWildcard1); $methodProphecy1->getMethodName()->willReturn('getUsername'); $methodProphecy2->getArgumentsWildcard()->willReturn($argumentsWildcard2); $methodProphecy2->getMethodName()->willReturn('getUsername'); $this->addMethodProphecy($methodProphecy1); $this->addMethodProphecy($methodProphecy2); $this->getMethodProphecies()->shouldReturn(array( 'getUsername' => array( $methodProphecy1, $methodProphecy2, ) )); } function its_addMethodProphecy_handles_prophecies_for_different_methods( MethodProphecy $methodProphecy1, MethodProphecy $methodProphecy2, ArgumentsWildcard $argumentsWildcard1, ArgumentsWildcard $argumentsWildcard2 ) { $methodProphecy1->getArgumentsWildcard()->willReturn($argumentsWildcard1); $methodProphecy1->getMethodName()->willReturn('getUsername'); $methodProphecy2->getArgumentsWildcard()->willReturn($argumentsWildcard2); $methodProphecy2->getMethodName()->willReturn('isUsername'); $this->addMethodProphecy($methodProphecy1); $this->addMethodProphecy($methodProphecy2); $this->getMethodProphecies()->shouldReturn(array( 'getUsername' => array( $methodProphecy1 ), 'isUsername' => array( $methodProphecy2 ) )); } function its_addMethodProphecy_throws_exception_when_method_has_no_ArgumentsWildcard(MethodProphecy $methodProphecy) { $methodProphecy->getArgumentsWildcard()->willReturn(null); $methodProphecy->getObjectProphecy()->willReturn($this); $methodProphecy->getMethodName()->willReturn('getTitle'); $this->shouldThrow('Prophecy\Exception\Prophecy\MethodProphecyException')->duringAddMethodProphecy( $methodProphecy ); } function it_returns_null_after_checkPredictions_call_if_there_is_no_method_prophecies() { $this->checkProphecyMethodsPredictions()->shouldReturn(null); } function it_throws_AggregateException_during_checkPredictions_if_predictions_fail( MethodProphecy $methodProphecy1, MethodProphecy $methodProphecy2, ArgumentsWildcard $argumentsWildcard1, ArgumentsWildcard $argumentsWildcard2 ) { $methodProphecy1->getMethodName()->willReturn('getName'); $methodProphecy1->getArgumentsWildcard()->willReturn($argumentsWildcard1); $methodProphecy1->checkPrediction() ->willThrow('Prophecy\Exception\Prediction\AggregateException'); $methodProphecy2->getMethodName()->willReturn('setName'); $methodProphecy2->getArgumentsWildcard()->willReturn($argumentsWildcard2); $methodProphecy2->checkPrediction() ->willThrow('Prophecy\Exception\Prediction\AggregateException'); $this->addMethodProphecy($methodProphecy1); $this->addMethodProphecy($methodProphecy2); $this->shouldThrow('Prophecy\Exception\Prediction\AggregateException') ->duringCheckProphecyMethodsPredictions(); } function it_returns_new_MethodProphecy_instance_for_arbitrary_call( Doubler $doubler, ProphecySubjectInterface $reflection ) { $doubler->double(Argument::any())->willReturn($reflection); $return = $this->getProphecy(); $return->shouldBeAnInstanceOf('Prophecy\Prophecy\MethodProphecy'); $return->getMethodName()->shouldReturn('getProphecy'); } function it_returns_same_MethodProphecy_for_same_registered_signature( Doubler $doubler, ProphecySubjectInterface $reflection ) { $doubler->double(Argument::any())->willReturn($reflection); $this->addMethodProphecy($methodProphecy1 = $this->getProphecy(1, 2, 3)); $methodProphecy2 = $this->getProphecy(1, 2, 3); $methodProphecy2->shouldBe($methodProphecy1); } function it_returns_new_MethodProphecy_for_different_signatures( Doubler $doubler, ProphecySubjectInterface $reflection ) { $doubler->double(Argument::any())->willReturn($reflection); $value = new ObjectProphecySpecFixtureB('ABC'); $value2 = new ObjectProphecySpecFixtureB('CBA'); $this->addMethodProphecy($methodProphecy1 = $this->getProphecy(1, 2, 3, $value)); $methodProphecy2 = $this->getProphecy(1, 2, 3, $value2); $methodProphecy2->shouldNotBe($methodProphecy1); } function it_returns_new_MethodProphecy_for_all_callback_signatures( Doubler $doubler, ProphecySubjectInterface $reflection ) { $doubler->double(Argument::any())->willReturn($reflection); $this->addMethodProphecy($methodProphecy1 = $this->getProphecy(function(){})); $methodProphecy2 = $this->getProphecy(function(){}); $methodProphecy2->shouldNotBe($methodProphecy1); } } class ObjectProphecySpecFixtureA { public $errors; } class ObjectProphecySpecFixtureB extends ObjectProphecySpecFixtureA { public $errors; public $value = null; public function __construct($value) { $this->value = $value; } } shouldBeAnInstanceOf('Prophecy\Prophecy\RevealerInterface'); } function it_reveals_single_instance_of_ProphecyInterface(ProphecyInterface $prophecy, \stdClass $object) { $prophecy->reveal()->willReturn($object); $this->reveal($prophecy)->shouldReturn($object); } function it_reveals_instances_of_ProphecyInterface_inside_array( ProphecyInterface $prophecy1, ProphecyInterface $prophecy2, \stdClass $object1, \stdClass $object2 ) { $prophecy1->reveal()->willReturn($object1); $prophecy2->reveal()->willReturn($object2); $this->reveal(array( array('item' => $prophecy2), $prophecy1 ))->shouldReturn(array( array('item' => $object2), $object1 )); } function it_does_not_touch_non_prophecy_interface() { $this->reveal(42)->shouldReturn(42); } } double(null, array())->willReturn($double); $this->beConstructedWith($doubler); } function it_constructs_new_prophecy_on_prophesize_call() { $prophecy = $this->prophesize(); $prophecy->shouldBeAnInstanceOf('Prophecy\Prophecy\ObjectProphecy'); } function it_constructs_new_prophecy_with_parent_class_if_specified($doubler, ProphecySubjectInterface $newDouble) { $doubler->double(Argument::any(), array())->willReturn($newDouble); $this->prophesize('Prophecy\Prophet')->reveal()->shouldReturn($newDouble); } function it_constructs_new_prophecy_with_interface_if_specified($doubler, ProphecySubjectInterface $newDouble) { $doubler->double(null, Argument::any())->willReturn($newDouble); $this->prophesize('ArrayAccess')->reveal()->shouldReturn($newDouble); } function it_exposes_all_created_prophecies_through_getter() { $prophecy1 = $this->prophesize(); $prophecy2 = $this->prophesize(); $this->getProphecies()->shouldReturn(array($prophecy1, $prophecy2)); } function it_does_nothing_during_checkPredictions_call_if_no_predictions_defined() { $this->checkPredictions()->shouldReturn(null); } function it_throws_AggregateException_if_defined_predictions_fail( MethodProphecy $method1, MethodProphecy $method2, ArgumentsWildcard $arguments1, ArgumentsWildcard $arguments2 ) { $method1->getMethodName()->willReturn('getName'); $method1->getArgumentsWildcard()->willReturn($arguments1); $method1->checkPrediction()->willReturn(null); $method2->getMethodName()->willReturn('isSet'); $method2->getArgumentsWildcard()->willReturn($arguments2); $method2->checkPrediction()->willThrow( 'Prophecy\Exception\Prediction\AggregateException' ); $this->prophesize()->addMethodProphecy($method1); $this->prophesize()->addMethodProphecy($method2); $this->shouldThrow('Prophecy\Exception\Prediction\AggregateException') ->duringCheckPredictions(); } function it_exposes_doubler_through_getter($doubler) { $this->getDoubler()->shouldReturn($doubler); } } stringify(42)->shouldReturn('42'); } function it_generates_proper_string_representation_for_string() { $this->stringify('some string')->shouldReturn('"some string"'); } function it_generates_single_line_representation_for_multiline_string() { $this->stringify("some\nstring")->shouldReturn('"some\\nstring"'); } function it_generates_proper_string_representation_for_double() { $this->stringify(42.3)->shouldReturn('42.3'); } function it_generates_proper_string_representation_for_boolean_true() { $this->stringify(true)->shouldReturn('true'); } function it_generates_proper_string_representation_for_boolean_false() { $this->stringify(false)->shouldReturn('false'); } function it_generates_proper_string_representation_for_null() { $this->stringify(null)->shouldReturn('null'); } function it_generates_proper_string_representation_for_empty_array() { $this->stringify(array())->shouldReturn('[]'); } function it_generates_proper_string_representation_for_array() { $this->stringify(array('zet', 42))->shouldReturn('["zet", 42]'); } function it_generates_proper_string_representation_for_hash_containing_one_value() { $this->stringify(array('ever' => 'zet'))->shouldReturn('["ever" => "zet"]'); } function it_generates_proper_string_representation_for_hash() { $this->stringify(array('ever' => 'zet', 52 => 'hey', 'num' => 42))->shouldReturn( '["ever" => "zet", 52 => "hey", "num" => 42]' ); } function it_generates_proper_string_representation_for_resource() { $resource = fopen(__FILE__, 'r'); $this->stringify($resource)->shouldReturn('stream:'.$resource); } function it_generates_proper_string_representation_for_object(\stdClass $object) { $objHash = sprintf('%s:%s', get_class($object->getWrappedObject()), spl_object_hash($object->getWrappedObject()) ) . " Object (\n 'objectProphecy' => Prophecy\Prophecy\ObjectProphecy Object (*Prophecy*)\n)"; $this->stringify($object)->shouldReturn("$objHash"); } function it_generates_proper_string_representation_for_object_without_exporting(\stdClass $object) { $objHash = sprintf('%s:%s', get_class($object->getWrappedObject()), spl_object_hash($object->getWrappedObject()) ); $this->stringify($object, false)->shouldReturn("$objHash"); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument; /** * Arguments wildcarding. * * @author Konstantin Kudryashov */ class ArgumentsWildcard { /** * @var Token\TokenInterface[] */ private $tokens = array(); private $string; /** * Initializes wildcard. * * @param array $arguments Array of argument tokens or values */ public function __construct(array $arguments) { foreach ($arguments as $argument) { if (!$argument instanceof Token\TokenInterface) { $argument = new Token\ExactValueToken($argument); } $this->tokens[] = $argument; } } /** * Calculates wildcard match score for provided arguments. * * @param array $arguments * * @return false|int False OR integer score (higher - better) */ public function scoreArguments(array $arguments) { if (0 == count($arguments) && 0 == count($this->tokens)) { return 1; } $arguments = array_values($arguments); $totalScore = 0; foreach ($this->tokens as $i => $token) { $argument = isset($arguments[$i]) ? $arguments[$i] : null; if (1 >= $score = $token->scoreArgument($argument)) { return false; } $totalScore += $score; if (true === $token->isLast()) { return $totalScore; } } if (count($arguments) > count($this->tokens)) { return false; } return $totalScore; } /** * Returns string representation for wildcard. * * @return string */ public function __toString() { if (null === $this->string) { $this->string = implode(', ', array_map(function ($token) { return (string) $token; }, $this->tokens)); } return $this->string; } /** * @return array */ public function getTokens() { return $this->tokens; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; /** * Any values token. * * @author Konstantin Kudryashov */ class AnyValuesToken implements TokenInterface { /** * Always scores 2 for any argument. * * @param $argument * * @return int */ public function scoreArgument($argument) { return 2; } /** * Returns true to stop wildcard from processing other tokens. * * @return bool */ public function isLast() { return true; } /** * Returns string representation for token. * * @return string */ public function __toString() { return '* [, ...]'; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; /** * Any single value token. * * @author Konstantin Kudryashov */ class AnyValueToken implements TokenInterface { /** * Always scores 3 for any argument. * * @param $argument * * @return int */ public function scoreArgument($argument) { return 3; } /** * Returns false. * * @return bool */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { return '*'; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; /** * Approximate value token * * @author Daniel Leech */ class ApproximateValueToken implements TokenInterface { private $value; private $precision; public function __construct($value, $precision = 0) { $this->value = $value; $this->precision = $precision; } /** * {@inheritdoc} */ public function scoreArgument($argument) { return round($argument, $this->precision) === round($this->value, $this->precision) ? 10 : false; } /** * {@inheritdoc} */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { return sprintf('≅%s', round($this->value, $this->precision)); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; /** * Array elements count token. * * @author Boris Mikhaylov */ class ArrayCountToken implements TokenInterface { private $count; /** * @param integer $value */ public function __construct($value) { $this->count = $value; } /** * Scores 6 when argument has preset number of elements. * * @param $argument * * @return bool|int */ public function scoreArgument($argument) { return $this->isCountable($argument) && $this->hasProperCount($argument) ? 6 : false; } /** * Returns false. * * @return boolean */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { return sprintf('count(%s)', $this->count); } /** * Returns true if object is either array or instance of \Countable * * @param $argument * @return bool */ private function isCountable($argument) { return (is_array($argument) || $argument instanceof \Countable); } /** * Returns true if $argument has expected number of elements * * @param array|\Countable $argument * * @return bool */ private function hasProperCount($argument) { return $this->count === count($argument); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; use Prophecy\Exception\InvalidArgumentException; /** * Array entry token. * * @author Boris Mikhaylov */ class ArrayEntryToken implements TokenInterface { /** @var \Prophecy\Argument\Token\TokenInterface */ private $key; /** @var \Prophecy\Argument\Token\TokenInterface */ private $value; /** * @param mixed $key exact value or token * @param mixed $value exact value or token */ public function __construct($key, $value) { $this->key = $this->wrapIntoExactValueToken($key); $this->value = $this->wrapIntoExactValueToken($value); } /** * Scores half of combined scores from key and value tokens for same entry. Capped at 8. * If argument implements \ArrayAccess without \Traversable, then key token is restricted to ExactValueToken. * * @param array|\ArrayAccess|\Traversable $argument * * @throws \Prophecy\Exception\InvalidArgumentException * @return bool|int */ public function scoreArgument($argument) { if ($argument instanceof \Traversable) { $argument = iterator_to_array($argument); } if ($argument instanceof \ArrayAccess) { $argument = $this->convertArrayAccessToEntry($argument); } if (!is_array($argument) || empty($argument)) { return false; } $keyScores = array_map(array($this->key,'scoreArgument'), array_keys($argument)); $valueScores = array_map(array($this->value,'scoreArgument'), $argument); $scoreEntry = function ($value, $key) { return $value && $key ? min(8, ($key + $value) / 2) : false; }; return max(array_map($scoreEntry, $valueScores, $keyScores)); } /** * Returns false. * * @return boolean */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { return sprintf('[..., %s => %s, ...]', $this->key, $this->value); } /** * Returns key * * @return TokenInterface */ public function getKey() { return $this->key; } /** * Returns value * * @return TokenInterface */ public function getValue() { return $this->value; } /** * Wraps non token $value into ExactValueToken * * @param $value * @return TokenInterface */ private function wrapIntoExactValueToken($value) { return $value instanceof TokenInterface ? $value : new ExactValueToken($value); } /** * Converts instance of \ArrayAccess to key => value array entry * * @param \ArrayAccess $object * * @return array|null * @throws \Prophecy\Exception\InvalidArgumentException */ private function convertArrayAccessToEntry(\ArrayAccess $object) { if (!$this->key instanceof ExactValueToken) { throw new InvalidArgumentException(sprintf( 'You can only use exact value tokens to match key of ArrayAccess object'.PHP_EOL. 'But you used `%s`.', $this->key )); } $key = $this->key->getValue(); return $object->offsetExists($key) ? array($key => $object[$key]) : array(); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; /** * Array every entry token. * * @author Adrien Brault */ class ArrayEveryEntryToken implements TokenInterface { /** * @var TokenInterface */ private $value; /** * @param mixed $value exact value or token */ public function __construct($value) { if (!$value instanceof TokenInterface) { $value = new ExactValueToken($value); } $this->value = $value; } /** * {@inheritdoc} */ public function scoreArgument($argument) { if (!$argument instanceof \Traversable && !is_array($argument)) { return false; } $scores = array(); foreach ($argument as $key => $argumentEntry) { $scores[] = $this->value->scoreArgument($argumentEntry); } if (empty($scores) || in_array(false, $scores, true)) { return false; } return array_sum($scores) / count($scores); } /** * {@inheritdoc} */ public function isLast() { return false; } /** * {@inheritdoc} */ public function __toString() { return sprintf('[%s, ..., %s]', $this->value, $this->value); } /** * @return TokenInterface */ public function getValue() { return $this->value; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; use Prophecy\Exception\InvalidArgumentException; /** * Callback-verified token. * * @author Konstantin Kudryashov */ class CallbackToken implements TokenInterface { private $callback; /** * Initializes token. * * @param callable $callback * * @throws \Prophecy\Exception\InvalidArgumentException */ public function __construct($callback) { if (!is_callable($callback)) { throw new InvalidArgumentException(sprintf( 'Callable expected as an argument to CallbackToken, but got %s.', gettype($callback) )); } $this->callback = $callback; } /** * Scores 7 if callback returns true, false otherwise. * * @param $argument * * @return bool|int */ public function scoreArgument($argument) { return call_user_func($this->callback, $argument) ? 7 : false; } /** * Returns false. * * @return bool */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { return 'callback()'; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; use SebastianBergmann\Comparator\ComparisonFailure; use Prophecy\Comparator\Factory as ComparatorFactory; use Prophecy\Util\StringUtil; /** * Exact value token. * * @author Konstantin Kudryashov */ class ExactValueToken implements TokenInterface { private $value; private $string; private $util; private $comparatorFactory; /** * Initializes token. * * @param mixed $value * @param StringUtil $util * @param ComparatorFactory $comparatorFactory */ public function __construct($value, StringUtil $util = null, ComparatorFactory $comparatorFactory = null) { $this->value = $value; $this->util = $util ?: new StringUtil(); $this->comparatorFactory = $comparatorFactory ?: ComparatorFactory::getInstance(); } /** * Scores 10 if argument matches preset value. * * @param $argument * * @return bool|int */ public function scoreArgument($argument) { if (is_object($argument) && is_object($this->value)) { $comparator = $this->comparatorFactory->getComparatorFor( $argument, $this->value ); try { $comparator->assertEquals($argument, $this->value); return 10; } catch (ComparisonFailure $failure) {} } // If either one is an object it should be castable to a string if (is_object($argument) xor is_object($this->value)) { if (is_object($argument) && !method_exists($argument, '__toString')) { return false; } if (is_object($this->value) && !method_exists($this->value, '__toString')) { return false; } } elseif (is_numeric($argument) && is_numeric($this->value)) { // noop } elseif (gettype($argument) !== gettype($this->value)) { return false; } return $argument == $this->value ? 10 : false; } /** * Returns preset value against which token checks arguments. * * @return mixed */ public function getValue() { return $this->value; } /** * Returns false. * * @return bool */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { if (null === $this->string) { $this->string = sprintf('exact(%s)', $this->util->stringify($this->value)); } return $this->string; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; use Prophecy\Util\StringUtil; /** * Identical value token. * * @author Florian Voutzinos */ class IdenticalValueToken implements TokenInterface { private $value; private $string; private $util; /** * Initializes token. * * @param mixed $value * @param StringUtil $util */ public function __construct($value, StringUtil $util = null) { $this->value = $value; $this->util = $util ?: new StringUtil(); } /** * Scores 11 if argument matches preset value. * * @param $argument * * @return bool|int */ public function scoreArgument($argument) { return $argument === $this->value ? 11 : false; } /** * Returns false. * * @return bool */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { if (null === $this->string) { $this->string = sprintf('identical(%s)', $this->util->stringify($this->value)); } return $this->string; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; /** * Logical AND token. * * @author Boris Mikhaylov */ class LogicalAndToken implements TokenInterface { private $tokens = array(); /** * @param array $arguments exact values or tokens */ public function __construct(array $arguments) { foreach ($arguments as $argument) { if (!$argument instanceof TokenInterface) { $argument = new ExactValueToken($argument); } $this->tokens[] = $argument; } } /** * Scores maximum score from scores returned by tokens for this argument if all of them score. * * @param $argument * * @return bool|int */ public function scoreArgument($argument) { if (0 === count($this->tokens)) { return false; } $maxScore = 0; foreach ($this->tokens as $token) { $score = $token->scoreArgument($argument); if (false === $score) { return false; } $maxScore = max($score, $maxScore); } return $maxScore; } /** * Returns false. * * @return boolean */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { return sprintf('bool(%s)', implode(' AND ', $this->tokens)); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; /** * Logical NOT token. * * @author Boris Mikhaylov */ class LogicalNotToken implements TokenInterface { /** @var \Prophecy\Argument\Token\TokenInterface */ private $token; /** * @param mixed $value exact value or token */ public function __construct($value) { $this->token = $value instanceof TokenInterface? $value : new ExactValueToken($value); } /** * Scores 4 when preset token does not match the argument. * * @param $argument * * @return bool|int */ public function scoreArgument($argument) { return false === $this->token->scoreArgument($argument) ? 4 : false; } /** * Returns true if preset token is last. * * @return bool|int */ public function isLast() { return $this->token->isLast(); } /** * Returns originating token. * * @return TokenInterface */ public function getOriginatingToken() { return $this->token; } /** * Returns string representation for token. * * @return string */ public function __toString() { return sprintf('not(%s)', $this->token); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; use SebastianBergmann\Comparator\ComparisonFailure; use Prophecy\Comparator\Factory as ComparatorFactory; use Prophecy\Util\StringUtil; /** * Object state-checker token. * * @author Konstantin Kudryashov */ class ObjectStateToken implements TokenInterface { private $name; private $value; private $util; private $comparatorFactory; /** * Initializes token. * * @param string $methodName * @param mixed $value Expected return value * @param null|StringUtil $util * @param ComparatorFactory $comparatorFactory */ public function __construct( $methodName, $value, StringUtil $util = null, ComparatorFactory $comparatorFactory = null ) { $this->name = $methodName; $this->value = $value; $this->util = $util ?: new StringUtil; $this->comparatorFactory = $comparatorFactory ?: ComparatorFactory::getInstance(); } /** * Scores 8 if argument is an object, which method returns expected value. * * @param mixed $argument * * @return bool|int */ public function scoreArgument($argument) { if (is_object($argument) && method_exists($argument, $this->name)) { $actual = call_user_func(array($argument, $this->name)); $comparator = $this->comparatorFactory->getComparatorFor( $this->value, $actual ); try { $comparator->assertEquals($this->value, $actual); return 8; } catch (ComparisonFailure $failure) { return false; } } if (is_object($argument) && property_exists($argument, $this->name)) { return $argument->{$this->name} === $this->value ? 8 : false; } return false; } /** * Returns false. * * @return bool */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { return sprintf('state(%s(), %s)', $this->name, $this->util->stringify($this->value) ); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; /** * String contains token. * * @author Peter Mitchell */ class StringContainsToken implements TokenInterface { private $value; /** * Initializes token. * * @param string $value */ public function __construct($value) { $this->value = $value; } public function scoreArgument($argument) { return strpos($argument, $this->value) !== false ? 6 : false; } /** * Returns preset value against which token checks arguments. * * @return mixed */ public function getValue() { return $this->value; } /** * Returns false. * * @return bool */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { return sprintf('contains("%s")', $this->value); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; /** * Argument token interface. * * @author Konstantin Kudryashov */ interface TokenInterface { /** * Calculates token match score for provided argument. * * @param $argument * * @return bool|int */ public function scoreArgument($argument); /** * Returns true if this token prevents check of other tokens (is last one). * * @return bool|int */ public function isLast(); /** * Returns string representation for token. * * @return string */ public function __toString(); } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Argument\Token; use Prophecy\Exception\InvalidArgumentException; /** * Value type token. * * @author Konstantin Kudryashov */ class TypeToken implements TokenInterface { private $type; /** * @param string $type */ public function __construct($type) { $checker = "is_{$type}"; if (!function_exists($checker) && !interface_exists($type) && !class_exists($type)) { throw new InvalidArgumentException(sprintf( 'Type or class name expected as an argument to TypeToken, but got %s.', $type )); } $this->type = $type; } /** * Scores 5 if argument has the same type this token was constructed with. * * @param $argument * * @return bool|int */ public function scoreArgument($argument) { $checker = "is_{$this->type}"; if (function_exists($checker)) { return call_user_func($checker, $argument) ? 5 : false; } return $argument instanceof $this->type ? 5 : false; } /** * Returns false. * * @return bool */ public function isLast() { return false; } /** * Returns string representation for token. * * @return string */ public function __toString() { return sprintf('type(%s)', $this->type); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy; use Prophecy\Argument\Token; /** * Argument tokens shortcuts. * * @author Konstantin Kudryashov */ class Argument { /** * Checks that argument is exact value or object. * * @param mixed $value * * @return Token\ExactValueToken */ public static function exact($value) { return new Token\ExactValueToken($value); } /** * Checks that argument is of specific type or instance of specific class. * * @param string $type Type name (`integer`, `string`) or full class name * * @return Token\TypeToken */ public static function type($type) { return new Token\TypeToken($type); } /** * Checks that argument object has specific state. * * @param string $methodName * @param mixed $value * * @return Token\ObjectStateToken */ public static function which($methodName, $value) { return new Token\ObjectStateToken($methodName, $value); } /** * Checks that argument matches provided callback. * * @param callable $callback * * @return Token\CallbackToken */ public static function that($callback) { return new Token\CallbackToken($callback); } /** * Matches any single value. * * @return Token\AnyValueToken */ public static function any() { return new Token\AnyValueToken; } /** * Matches all values to the rest of the signature. * * @return Token\AnyValuesToken */ public static function cetera() { return new Token\AnyValuesToken; } /** * Checks that argument matches all tokens * * @param mixed ... a list of tokens * * @return Token\LogicalAndToken */ public static function allOf() { return new Token\LogicalAndToken(func_get_args()); } /** * Checks that argument array or countable object has exact number of elements. * * @param integer $value array elements count * * @return Token\ArrayCountToken */ public static function size($value) { return new Token\ArrayCountToken($value); } /** * Checks that argument array contains (key, value) pair * * @param mixed $key exact value or token * @param mixed $value exact value or token * * @return Token\ArrayEntryToken */ public static function withEntry($key, $value) { return new Token\ArrayEntryToken($key, $value); } /** * Checks that arguments array entries all match value * * @param mixed $value * * @return Token\ArrayEveryEntryToken */ public static function withEveryEntry($value) { return new Token\ArrayEveryEntryToken($value); } /** * Checks that argument array contains value * * @param mixed $value * * @return Token\ArrayEntryToken */ public static function containing($value) { return new Token\ArrayEntryToken(self::any(), $value); } /** * Checks that argument array has key * * @param mixed $key exact value or token * * @return Token\ArrayEntryToken */ public static function withKey($key) { return new Token\ArrayEntryToken($key, self::any()); } /** * Checks that argument does not match the value|token. * * @param mixed $value either exact value or argument token * * @return Token\LogicalNotToken */ public static function not($value) { return new Token\LogicalNotToken($value); } /** * @param string $value * * @return Token\StringContainsToken */ public static function containingString($value) { return new Token\StringContainsToken($value); } /** * Checks that argument is identical value. * * @param mixed $value * * @return Token\IdenticalValueToken */ public static function is($value) { return new Token\IdenticalValueToken($value); } /** * Check that argument is same value when rounding to the * given precision. * * @param float $value * @param float $precision * * @return Token\ApproximateValueToken */ public static function approximate($value, $precision = 0) { return new Token\ApproximateValueToken($value, $precision); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Call; use Exception; /** * Call object. * * @author Konstantin Kudryashov */ class Call { private $methodName; private $arguments; private $returnValue; private $exception; private $file; private $line; /** * Initializes call. * * @param string $methodName * @param array $arguments * @param mixed $returnValue * @param Exception $exception * @param null|string $file * @param null|int $line */ public function __construct($methodName, array $arguments, $returnValue, Exception $exception = null, $file, $line) { $this->methodName = $methodName; $this->arguments = $arguments; $this->returnValue = $returnValue; $this->exception = $exception; if ($file) { $this->file = $file; $this->line = intval($line); } } /** * Returns called method name. * * @return string */ public function getMethodName() { return $this->methodName; } /** * Returns called method arguments. * * @return array */ public function getArguments() { return $this->arguments; } /** * Returns called method return value. * * @return null|mixed */ public function getReturnValue() { return $this->returnValue; } /** * Returns exception that call thrown. * * @return null|Exception */ public function getException() { return $this->exception; } /** * Returns callee filename. * * @return string */ public function getFile() { return $this->file; } /** * Returns callee line number. * * @return int */ public function getLine() { return $this->line; } /** * Returns short notation for callee place. * * @return string */ public function getCallPlace() { if (null === $this->file) { return 'unknown'; } return sprintf('%s:%d', $this->file, $this->line); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Call; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Argument\ArgumentsWildcard; use Prophecy\Util\StringUtil; use Prophecy\Exception\Call\UnexpectedCallException; /** * Calls receiver & manager. * * @author Konstantin Kudryashov */ class CallCenter { private $util; /** * @var Call[] */ private $recordedCalls = array(); /** * Initializes call center. * * @param StringUtil $util */ public function __construct(StringUtil $util = null) { $this->util = $util ?: new StringUtil; } /** * Makes and records specific method call for object prophecy. * * @param ObjectProphecy $prophecy * @param string $methodName * @param array $arguments * * @return mixed Returns null if no promise for prophecy found or promise return value. * * @throws \Prophecy\Exception\Call\UnexpectedCallException If no appropriate method prophecy found */ public function makeCall(ObjectProphecy $prophecy, $methodName, array $arguments) { // For efficiency exclude 'args' from the generated backtrace if (PHP_VERSION_ID >= 50400) { // Limit backtrace to last 3 calls as we don't use the rest // Limit argument was introduced in PHP 5.4.0 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); } elseif (defined('DEBUG_BACKTRACE_IGNORE_ARGS')) { // DEBUG_BACKTRACE_IGNORE_ARGS was introduced in PHP 5.3.6 $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); } else { $backtrace = debug_backtrace(); } $file = $line = null; if (isset($backtrace[2]) && isset($backtrace[2]['file'])) { $file = $backtrace[2]['file']; $line = $backtrace[2]['line']; } // If no method prophecies defined, then it's a dummy, so we'll just return null if ('__destruct' === $methodName || 0 == count($prophecy->getMethodProphecies())) { $this->recordedCalls[] = new Call($methodName, $arguments, null, null, $file, $line); return null; } // There are method prophecies, so it's a fake/stub. Searching prophecy for this call $matches = array(); foreach ($prophecy->getMethodProphecies($methodName) as $methodProphecy) { if (0 < $score = $methodProphecy->getArgumentsWildcard()->scoreArguments($arguments)) { $matches[] = array($score, $methodProphecy); } } // If fake/stub doesn't have method prophecy for this call - throw exception if (!count($matches)) { throw $this->createUnexpectedCallException($prophecy, $methodName, $arguments); } // Sort matches by their score value @usort($matches, function ($match1, $match2) { return $match2[0] - $match1[0]; }); // If Highest rated method prophecy has a promise - execute it or return null instead $returnValue = null; $exception = null; if ($promise = $matches[0][1]->getPromise()) { try { $returnValue = $promise->execute($arguments, $prophecy, $matches[0][1]); } catch (\Exception $e) { $exception = $e; } } $this->recordedCalls[] = new Call( $methodName, $arguments, $returnValue, $exception, $file, $line ); if (null !== $exception) { throw $exception; } return $returnValue; } /** * Searches for calls by method name & arguments wildcard. * * @param string $methodName * @param ArgumentsWildcard $wildcard * * @return Call[] */ public function findCalls($methodName, ArgumentsWildcard $wildcard) { return array_values( array_filter($this->recordedCalls, function (Call $call) use ($methodName, $wildcard) { return $methodName === $call->getMethodName() && 0 < $wildcard->scoreArguments($call->getArguments()) ; }) ); } private function createUnexpectedCallException(ObjectProphecy $prophecy, $methodName, array $arguments) { $classname = get_class($prophecy->reveal()); $argstring = implode(', ', array_map(array($this->util, 'stringify'), $arguments)); $expected = implode("\n", array_map(function (MethodProphecy $methodProphecy) { return sprintf(' - %s(%s)', $methodProphecy->getMethodName(), $methodProphecy->getArgumentsWildcard() ); }, call_user_func_array('array_merge', $prophecy->getMethodProphecies()))); return new UnexpectedCallException( sprintf( "Method call:\n". " - %s(%s)\n". "on %s was not expected, expected calls were:\n%s", $methodName, $argstring, $classname, $expected ), $prophecy, $methodName, $arguments ); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Comparator; use SebastianBergmann\Comparator\Comparator; use SebastianBergmann\Comparator\ComparisonFailure; /** * Closure comparator. * * @author Konstantin Kudryashov */ final class ClosureComparator extends Comparator { public function accepts($expected, $actual) { return is_object($expected) && $expected instanceof \Closure && is_object($actual) && $actual instanceof \Closure; } public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) { throw new ComparisonFailure( $expected, $actual, // we don't need a diff '', '', false, 'all closures are born different' ); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Comparator; use SebastianBergmann\Comparator\Factory as BaseFactory; /** * Prophecy comparator factory. * * @author Konstantin Kudryashov */ final class Factory extends BaseFactory { /** * @var Factory */ private static $instance; public function __construct() { parent::__construct(); $this->register(new ClosureComparator()); $this->register(new ProphecyComparator()); } /** * @return Factory */ public static function getInstance() { if (self::$instance === null) { self::$instance = new Factory; } return self::$instance; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Comparator; use Prophecy\Prophecy\ProphecyInterface; use SebastianBergmann\Comparator\ObjectComparator; class ProphecyComparator extends ObjectComparator { public function accepts($expected, $actual) { return is_object($expected) && is_object($actual) && $actual instanceof ProphecyInterface; } public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false, array &$processed = array()) { parent::assertEquals($expected, $actual->reveal(), $delta, $canonicalize, $ignoreCase, $processed); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler; use ReflectionClass; /** * Cached class doubler. * Prevents mirroring/creation of the same structure twice. * * @author Konstantin Kudryashov */ class CachedDoubler extends Doubler { private $classes = array(); /** * {@inheritdoc} */ public function registerClassPatch(ClassPatch\ClassPatchInterface $patch) { $this->classes[] = array(); parent::registerClassPatch($patch); } /** * {@inheritdoc} */ protected function createDoubleClass(ReflectionClass $class = null, array $interfaces) { $classId = $this->generateClassId($class, $interfaces); if (isset($this->classes[$classId])) { return $this->classes[$classId]; } return $this->classes[$classId] = parent::createDoubleClass($class, $interfaces); } /** * @param ReflectionClass $class * @param ReflectionClass[] $interfaces * * @return string */ private function generateClassId(ReflectionClass $class = null, array $interfaces) { $parts = array(); if (null !== $class) { $parts[] = $class->getName(); } foreach ($interfaces as $interface) { $parts[] = $interface->getName(); } sort($parts); return md5(implode('', $parts)); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\ClassPatch; use Prophecy\Doubler\Generator\Node\ClassNode; /** * Class patch interface. * Class patches extend doubles functionality or help * Prophecy to avoid some internal PHP bugs. * * @author Konstantin Kudryashov */ interface ClassPatchInterface { /** * Checks if patch supports specific class node. * * @param ClassNode $node * * @return bool */ public function supports(ClassNode $node); /** * Applies patch to the specific class node. * * @param ClassNode $node * @return void */ public function apply(ClassNode $node); /** * Returns patch priority, which determines when patch will be applied. * * @return int Priority number (higher - earlier) */ public function getPriority(); } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\ClassPatch; use Prophecy\Doubler\Generator\Node\ClassNode; use Prophecy\Doubler\Generator\Node\MethodNode; /** * Disable constructor. * Makes all constructor arguments optional. * * @author Konstantin Kudryashov */ class DisableConstructorPatch implements ClassPatchInterface { /** * Checks if class has `__construct` method. * * @param ClassNode $node * * @return bool */ public function supports(ClassNode $node) { return true; } /** * Makes all class constructor arguments optional. * * @param ClassNode $node */ public function apply(ClassNode $node) { if (!$node->hasMethod('__construct')) { $node->addMethod(new MethodNode('__construct', '')); return; } $constructor = $node->getMethod('__construct'); foreach ($constructor->getArguments() as $argument) { $argument->setDefault(null); } $constructor->setCode(<< * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\ClassPatch; use Prophecy\Doubler\Generator\Node\ClassNode; /** * Exception patch for HHVM to remove the stubs from special methods * * @author Christophe Coevoet */ class HhvmExceptionPatch implements ClassPatchInterface { /** * Supports exceptions on HHVM. * * @param ClassNode $node * * @return bool */ public function supports(ClassNode $node) { if (!defined('HHVM_VERSION')) { return false; } return 'Exception' === $node->getParentClass() || is_subclass_of($node->getParentClass(), 'Exception'); } /** * Removes special exception static methods from the doubled methods. * * @param ClassNode $node * * @return void */ public function apply(ClassNode $node) { if ($node->hasMethod('setTraceOptions')) { $node->getMethod('setTraceOptions')->useParentCode(); } if ($node->hasMethod('getTraceOptions')) { $node->getMethod('getTraceOptions')->useParentCode(); } } /** * {@inheritdoc} */ public function getPriority() { return -50; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\ClassPatch; use Prophecy\Doubler\Generator\Node\ClassNode; /** * Remove method functionality from the double which will clash with php keywords. * * @author Milan Magudia */ class KeywordPatch implements ClassPatchInterface { /** * Support any class * * @param ClassNode $node * * @return boolean */ public function supports(ClassNode $node) { return true; } /** * Remove methods that clash with php keywords * * @param ClassNode $node */ public function apply(ClassNode $node) { $methodNames = array_keys($node->getMethods()); $methodsToRemove = array_intersect($methodNames, $this->getKeywords()); foreach ($methodsToRemove as $methodName) { $node->removeMethod($methodName); } } /** * Returns patch priority, which determines when patch will be applied. * * @return int Priority number (higher - earlier) */ public function getPriority() { return 49; } /** * Returns array of php keywords. * * @return array */ private function getKeywords() { return array( '__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'finally', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', 'yield', ); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\ClassPatch; use Prophecy\Doubler\Generator\Node\ClassNode; use Prophecy\Doubler\Generator\Node\MethodNode; use Prophecy\PhpDocumentor\ClassAndInterfaceTagRetriever; use Prophecy\PhpDocumentor\MethodTagRetrieverInterface; /** * Discover Magical API using "@method" PHPDoc format. * * @author Thomas Tourlourat * @author Kévin Dunglas * @author Théo FIDRY */ class MagicCallPatch implements ClassPatchInterface { private $tagRetriever; public function __construct(MethodTagRetrieverInterface $tagRetriever = null) { $this->tagRetriever = null === $tagRetriever ? new ClassAndInterfaceTagRetriever() : $tagRetriever; } /** * Support any class * * @param ClassNode $node * * @return boolean */ public function supports(ClassNode $node) { return true; } /** * Discover Magical API * * @param ClassNode $node */ public function apply(ClassNode $node) { $types = array_filter($node->getInterfaces(), function ($interface) { return 0 !== strpos($interface, 'Prophecy\\'); }); $types[] = $node->getParentClass(); foreach ($types as $type) { $reflectionClass = new \ReflectionClass($type); $tagList = $this->tagRetriever->getTagList($reflectionClass); foreach($tagList as $tag) { $methodName = $tag->getMethodName(); if (empty($methodName)) { continue; } if (!$reflectionClass->hasMethod($methodName)) { $methodNode = new MethodNode($methodName); $methodNode->setStatic($tag->isStatic()); $node->addMethod($methodNode); } } } } /** * Returns patch priority, which determines when patch will be applied. * * @return integer Priority number (higher - earlier) */ public function getPriority() { return 50; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\ClassPatch; use Prophecy\Doubler\Generator\Node\ClassNode; use Prophecy\Doubler\Generator\Node\MethodNode; use Prophecy\Doubler\Generator\Node\ArgumentNode; /** * Add Prophecy functionality to the double. * This is a core class patch for Prophecy. * * @author Konstantin Kudryashov */ class ProphecySubjectPatch implements ClassPatchInterface { /** * Always returns true. * * @param ClassNode $node * * @return bool */ public function supports(ClassNode $node) { return true; } /** * Apply Prophecy functionality to class node. * * @param ClassNode $node */ public function apply(ClassNode $node) { $node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface'); $node->addProperty('objectProphecy', 'private'); foreach ($node->getMethods() as $name => $method) { if ('__construct' === strtolower($name)) { continue; } $method->setCode( 'return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, func_get_args());' ); } $prophecySetter = new MethodNode('setProphecy'); $prophecyArgument = new ArgumentNode('prophecy'); $prophecyArgument->setTypeHint('Prophecy\Prophecy\ProphecyInterface'); $prophecySetter->addArgument($prophecyArgument); $prophecySetter->setCode('$this->objectProphecy = $prophecy;'); $prophecyGetter = new MethodNode('getProphecy'); $prophecyGetter->setCode('return $this->objectProphecy;'); if ($node->hasMethod('__call')) { $__call = $node->getMethod('__call'); } else { $__call = new MethodNode('__call'); $__call->addArgument(new ArgumentNode('name')); $__call->addArgument(new ArgumentNode('arguments')); $node->addMethod($__call); } $__call->setCode(<<getProphecy(), func_get_arg(0) ); PHP ); $node->addMethod($prophecySetter); $node->addMethod($prophecyGetter); } /** * Returns patch priority, which determines when patch will be applied. * * @return int Priority number (higher - earlier) */ public function getPriority() { return 0; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\ClassPatch; use Prophecy\Doubler\Generator\Node\ClassNode; /** * ReflectionClass::newInstance patch. * Makes first argument of newInstance optional, since it works but signature is misleading * * @author Florian Klein */ class ReflectionClassNewInstancePatch implements ClassPatchInterface { /** * Supports ReflectionClass * * @param ClassNode $node * * @return bool */ public function supports(ClassNode $node) { return 'ReflectionClass' === $node->getParentClass(); } /** * Updates newInstance's first argument to make it optional * * @param ClassNode $node */ public function apply(ClassNode $node) { foreach ($node->getMethod('newInstance')->getArguments() as $argument) { $argument->setDefault(null); } } /** * Returns patch priority, which determines when patch will be applied. * * @return int Priority number (higher = earlier) */ public function getPriority() { return 50; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\ClassPatch; use Prophecy\Doubler\Generator\Node\ClassNode; use Prophecy\Doubler\Generator\Node\MethodNode; /** * SplFileInfo patch. * Makes SplFileInfo and derivative classes usable with Prophecy. * * @author Konstantin Kudryashov */ class SplFileInfoPatch implements ClassPatchInterface { /** * Supports everything that extends SplFileInfo. * * @param ClassNode $node * * @return bool */ public function supports(ClassNode $node) { if (null === $node->getParentClass()) { return false; } return 'SplFileInfo' === $node->getParentClass() || is_subclass_of($node->getParentClass(), 'SplFileInfo') ; } /** * Updated constructor code to call parent one with dummy file argument. * * @param ClassNode $node */ public function apply(ClassNode $node) { if ($node->hasMethod('__construct')) { $constructor = $node->getMethod('__construct'); } else { $constructor = new MethodNode('__construct'); $node->addMethod($constructor); } if ($this->nodeIsDirectoryIterator($node)) { $constructor->setCode('return parent::__construct("' . __DIR__ . '");'); return; } if ($this->nodeIsSplFileObject($node)) { $constructor->setCode('return parent::__construct("' . __FILE__ .'");'); return; } $constructor->useParentCode(); } /** * Returns patch priority, which determines when patch will be applied. * * @return int Priority number (higher - earlier) */ public function getPriority() { return 50; } /** * @param ClassNode $node * @return boolean */ private function nodeIsDirectoryIterator(ClassNode $node) { $parent = $node->getParentClass(); return 'DirectoryIterator' === $parent || is_subclass_of($parent, 'DirectoryIterator'); } /** * @param ClassNode $node * @return boolean */ private function nodeIsSplFileObject(ClassNode $node) { $parent = $node->getParentClass(); return 'SplFileObject' === $parent || is_subclass_of($parent, 'SplFileObject'); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\ClassPatch; use Prophecy\Doubler\Generator\Node\ClassNode; use Prophecy\Doubler\Generator\Node\MethodNode; /** * Traversable interface patch. * Forces classes that implement interfaces, that extend Traversable to also implement Iterator. * * @author Konstantin Kudryashov */ class TraversablePatch implements ClassPatchInterface { /** * Supports nodetree, that implement Traversable, but not Iterator or IteratorAggregate. * * @param ClassNode $node * * @return bool */ public function supports(ClassNode $node) { if (in_array('Iterator', $node->getInterfaces())) { return false; } if (in_array('IteratorAggregate', $node->getInterfaces())) { return false; } foreach ($node->getInterfaces() as $interface) { if ('Traversable' !== $interface && !is_subclass_of($interface, 'Traversable')) { continue; } if ('Iterator' === $interface || is_subclass_of($interface, 'Iterator')) { continue; } if ('IteratorAggregate' === $interface || is_subclass_of($interface, 'IteratorAggregate')) { continue; } return true; } return false; } /** * Forces class to implement Iterator interface. * * @param ClassNode $node */ public function apply(ClassNode $node) { $node->addInterface('Iterator'); $node->addMethod(new MethodNode('current')); $node->addMethod(new MethodNode('key')); $node->addMethod(new MethodNode('next')); $node->addMethod(new MethodNode('rewind')); $node->addMethod(new MethodNode('valid')); } /** * Returns patch priority, which determines when patch will be applied. * * @return int Priority number (higher - earlier) */ public function getPriority() { return 100; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler; /** * Core double interface. * All doubled classes will implement this one. * * @author Konstantin Kudryashov */ interface DoubleInterface { } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler; use Doctrine\Instantiator\Instantiator; use Prophecy\Doubler\ClassPatch\ClassPatchInterface; use Prophecy\Doubler\Generator\ClassMirror; use Prophecy\Doubler\Generator\ClassCreator; use Prophecy\Exception\InvalidArgumentException; use ReflectionClass; /** * Cached class doubler. * Prevents mirroring/creation of the same structure twice. * * @author Konstantin Kudryashov */ class Doubler { private $mirror; private $creator; private $namer; /** * @var ClassPatchInterface[] */ private $patches = array(); /** * @var \Doctrine\Instantiator\Instantiator */ private $instantiator; /** * Initializes doubler. * * @param ClassMirror $mirror * @param ClassCreator $creator * @param NameGenerator $namer */ public function __construct(ClassMirror $mirror = null, ClassCreator $creator = null, NameGenerator $namer = null) { $this->mirror = $mirror ?: new ClassMirror; $this->creator = $creator ?: new ClassCreator; $this->namer = $namer ?: new NameGenerator; } /** * Returns list of registered class patches. * * @return ClassPatchInterface[] */ public function getClassPatches() { return $this->patches; } /** * Registers new class patch. * * @param ClassPatchInterface $patch */ public function registerClassPatch(ClassPatchInterface $patch) { $this->patches[] = $patch; @usort($this->patches, function (ClassPatchInterface $patch1, ClassPatchInterface $patch2) { return $patch2->getPriority() - $patch1->getPriority(); }); } /** * Creates double from specific class or/and list of interfaces. * * @param ReflectionClass $class * @param ReflectionClass[] $interfaces Array of ReflectionClass instances * @param array $args Constructor arguments * * @return DoubleInterface * * @throws \Prophecy\Exception\InvalidArgumentException */ public function double(ReflectionClass $class = null, array $interfaces, array $args = null) { foreach ($interfaces as $interface) { if (!$interface instanceof ReflectionClass) { throw new InvalidArgumentException(sprintf( "[ReflectionClass \$interface1 [, ReflectionClass \$interface2]] array expected as\n". "a second argument to `Doubler::double(...)`, but got %s.", is_object($interface) ? get_class($interface).' class' : gettype($interface) )); } } $classname = $this->createDoubleClass($class, $interfaces); $reflection = new ReflectionClass($classname); if (null !== $args) { return $reflection->newInstanceArgs($args); } if ((null === $constructor = $reflection->getConstructor()) || ($constructor->isPublic() && !$constructor->isFinal())) { return $reflection->newInstance(); } if (!$this->instantiator) { $this->instantiator = new Instantiator(); } return $this->instantiator->instantiate($classname); } /** * Creates double class and returns its FQN. * * @param ReflectionClass $class * @param ReflectionClass[] $interfaces * * @return string */ protected function createDoubleClass(ReflectionClass $class = null, array $interfaces) { $name = $this->namer->name($class, $interfaces); $node = $this->mirror->reflect($class, $interfaces); foreach ($this->patches as $patch) { if ($patch->supports($node)) { $patch->apply($node); } } $this->creator->create($name, $node); return $name; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\Generator; /** * Class code creator. * Generates PHP code for specific class node tree. * * @author Konstantin Kudryashov */ class ClassCodeGenerator { /** * Generates PHP code for class node. * * @param string $classname * @param Node\ClassNode $class * * @return string */ public function generate($classname, Node\ClassNode $class) { $parts = explode('\\', $classname); $classname = array_pop($parts); $namespace = implode('\\', $parts); $code = sprintf("class %s extends \%s implements %s {\n", $classname, $class->getParentClass(), implode(', ', array_map(function ($interface) {return '\\'.$interface;}, $class->getInterfaces()) ) ); foreach ($class->getProperties() as $name => $visibility) { $code .= sprintf("%s \$%s;\n", $visibility, $name); } $code .= "\n"; foreach ($class->getMethods() as $method) { $code .= $this->generateMethod($method)."\n"; } $code .= "\n}"; return sprintf("namespace %s {\n%s\n}", $namespace, $code); } private function generateMethod(Node\MethodNode $method) { $php = sprintf("%s %s function %s%s(%s)%s {\n", $method->getVisibility(), $method->isStatic() ? 'static' : '', $method->returnsReference() ? '&':'', $method->getName(), implode(', ', $this->generateArguments($method->getArguments())), version_compare(PHP_VERSION, '7.0', '>=') && $method->hasReturnType() ? sprintf(': %s', $method->getReturnType()) : '' ); $php .= $method->getCode()."\n"; return $php.'}'; } private function generateArguments(array $arguments) { return array_map(function (Node\ArgumentNode $argument) { $php = ''; if ($hint = $argument->getTypeHint()) { switch ($hint) { case 'array': case 'callable': $php .= $hint; break; case 'string': case 'int': case 'float': case 'bool': if (version_compare(PHP_VERSION, '7.0', '>=')) { $php .= $hint; break; } // Fall-through to default case for PHP 5.x default: $php .= '\\'.$hint; } } $php .= ' '.($argument->isPassedByReference() ? '&' : ''); $php .= $argument->isVariadic() ? '...' : ''; $php .= '$'.$argument->getName(); if ($argument->isOptional() && !$argument->isVariadic()) { $php .= ' = '.var_export($argument->getDefault(), true); } return $php; }, $arguments); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\Generator; use Prophecy\Exception\Doubler\ClassCreatorException; /** * Class creator. * Creates specific class in current environment. * * @author Konstantin Kudryashov */ class ClassCreator { private $generator; /** * Initializes creator. * * @param ClassCodeGenerator $generator */ public function __construct(ClassCodeGenerator $generator = null) { $this->generator = $generator ?: new ClassCodeGenerator; } /** * Creates class. * * @param string $classname * @param Node\ClassNode $class * * @return mixed * * @throws \Prophecy\Exception\Doubler\ClassCreatorException */ public function create($classname, Node\ClassNode $class) { $code = $this->generator->generate($classname, $class); $return = eval($code); if (!class_exists($classname, false)) { if (count($class->getInterfaces())) { throw new ClassCreatorException(sprintf( 'Could not double `%s` and implement interfaces: [%s].', $class->getParentClass(), implode(', ', $class->getInterfaces()) ), $class); } throw new ClassCreatorException( sprintf('Could not double `%s`.', $class->getParentClass()), $class ); } return $return; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\Generator; use Prophecy\Exception\InvalidArgumentException; use Prophecy\Exception\Doubler\ClassMirrorException; use ReflectionClass; use ReflectionMethod; use ReflectionParameter; /** * Class mirror. * Core doubler class. Mirrors specific class and/or interfaces into class node tree. * * @author Konstantin Kudryashov */ class ClassMirror { private static $reflectableMethods = array( '__construct', '__destruct', '__sleep', '__wakeup', '__toString', '__call', '__invoke' ); /** * Reflects provided arguments into class node. * * @param ReflectionClass $class * @param ReflectionClass[] $interfaces * * @return Node\ClassNode * * @throws \Prophecy\Exception\InvalidArgumentException */ public function reflect(ReflectionClass $class = null, array $interfaces) { $node = new Node\ClassNode; if (null !== $class) { if (true === $class->isInterface()) { throw new InvalidArgumentException(sprintf( "Could not reflect %s as a class, because it\n". "is interface - use the second argument instead.", $class->getName() )); } $this->reflectClassToNode($class, $node); } foreach ($interfaces as $interface) { if (!$interface instanceof ReflectionClass) { throw new InvalidArgumentException(sprintf( "[ReflectionClass \$interface1 [, ReflectionClass \$interface2]] array expected as\n". "a second argument to `ClassMirror::reflect(...)`, but got %s.", is_object($interface) ? get_class($interface).' class' : gettype($interface) )); } if (false === $interface->isInterface()) { throw new InvalidArgumentException(sprintf( "Could not reflect %s as an interface, because it\n". "is class - use the first argument instead.", $interface->getName() )); } $this->reflectInterfaceToNode($interface, $node); } $node->addInterface('Prophecy\Doubler\Generator\ReflectionInterface'); return $node; } private function reflectClassToNode(ReflectionClass $class, Node\ClassNode $node) { if (true === $class->isFinal()) { throw new ClassMirrorException(sprintf( 'Could not reflect class %s as it is marked final.', $class->getName() ), $class); } $node->setParentClass($class->getName()); foreach ($class->getMethods(ReflectionMethod::IS_ABSTRACT) as $method) { if (false === $method->isProtected()) { continue; } $this->reflectMethodToNode($method, $node); } foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { if (0 === strpos($method->getName(), '_') && !in_array($method->getName(), self::$reflectableMethods)) { continue; } if (true === $method->isFinal()) { $node->addUnextendableMethod($method->getName()); continue; } $this->reflectMethodToNode($method, $node); } } private function reflectInterfaceToNode(ReflectionClass $interface, Node\ClassNode $node) { $node->addInterface($interface->getName()); foreach ($interface->getMethods() as $method) { $this->reflectMethodToNode($method, $node); } } private function reflectMethodToNode(ReflectionMethod $method, Node\ClassNode $classNode) { $node = new Node\MethodNode($method->getName()); if (true === $method->isProtected()) { $node->setVisibility('protected'); } if (true === $method->isStatic()) { $node->setStatic(); } if (true === $method->returnsReference()) { $node->setReturnsReference(); } if (version_compare(PHP_VERSION, '7.0', '>=') && true === $method->hasReturnType()) { $returnType = (string) $method->getReturnType(); $returnTypeLower = strtolower($returnType); if ('self' === $returnTypeLower) { $returnType = $method->getDeclaringClass()->getName(); } if ('parent' === $returnTypeLower) { $returnType = $method->getDeclaringClass()->getParentClass()->getName(); } $node->setReturnType($returnType); } if (is_array($params = $method->getParameters()) && count($params)) { foreach ($params as $param) { $this->reflectArgumentToNode($param, $node); } } $classNode->addMethod($node); } private function reflectArgumentToNode(ReflectionParameter $parameter, Node\MethodNode $methodNode) { $name = $parameter->getName() == '...' ? '__dot_dot_dot__' : $parameter->getName(); $node = new Node\ArgumentNode($name); $node->setTypeHint($this->getTypeHint($parameter)); if ($this->isVariadic($parameter)) { $node->setAsVariadic(); } if ($this->hasDefaultValue($parameter)) { $node->setDefault($this->getDefaultValue($parameter)); } if ($parameter->isPassedByReference()) { $node->setAsPassedByReference(); } $methodNode->addArgument($node); } private function hasDefaultValue(ReflectionParameter $parameter) { if ($this->isVariadic($parameter)) { return false; } if ($parameter->isDefaultValueAvailable()) { return true; } return $parameter->isOptional() || $this->isNullable($parameter); } private function getDefaultValue(ReflectionParameter $parameter) { if (!$parameter->isDefaultValueAvailable()) { return null; } return $parameter->getDefaultValue(); } private function getTypeHint(ReflectionParameter $parameter) { if (null !== $className = $this->getParameterClassName($parameter)) { return $className; } if (true === $parameter->isArray()) { return 'array'; } if (version_compare(PHP_VERSION, '5.4', '>=') && true === $parameter->isCallable()) { return 'callable'; } if (version_compare(PHP_VERSION, '7.0', '>=') && true === $parameter->hasType()) { return (string) $parameter->getType(); } return null; } private function isVariadic(ReflectionParameter $parameter) { return PHP_VERSION_ID >= 50600 && $parameter->isVariadic(); } private function isNullable(ReflectionParameter $parameter) { return $parameter->allowsNull() && null !== $this->getTypeHint($parameter); } private function getParameterClassName(ReflectionParameter $parameter) { try { return $parameter->getClass() ? $parameter->getClass()->getName() : null; } catch (\ReflectionException $e) { preg_match('/\[\s\<\w+?>\s([\w,\\\]+)/s', $parameter, $matches); return isset($matches[1]) ? $matches[1] : null; } } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\Generator\Node; /** * Argument node. * * @author Konstantin Kudryashov */ class ArgumentNode { private $name; private $typeHint; private $default; private $optional = false; private $byReference = false; private $isVariadic = false; /** * @param string $name */ public function __construct($name) { $this->name = $name; } public function getName() { return $this->name; } public function getTypeHint() { return $this->typeHint; } public function setTypeHint($typeHint = null) { $this->typeHint = $typeHint; } public function hasDefault() { return $this->isOptional() && !$this->isVariadic(); } public function getDefault() { return $this->default; } public function setDefault($default = null) { $this->optional = true; $this->default = $default; } public function isOptional() { return $this->optional; } public function setAsPassedByReference($byReference = true) { $this->byReference = $byReference; } public function isPassedByReference() { return $this->byReference; } public function setAsVariadic($isVariadic = true) { $this->isVariadic = $isVariadic; } public function isVariadic() { return $this->isVariadic; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\Generator\Node; use Prophecy\Exception\Doubler\MethodNotExtendableException; use Prophecy\Exception\InvalidArgumentException; /** * Class node. * * @author Konstantin Kudryashov */ class ClassNode { private $parentClass = 'stdClass'; private $interfaces = array(); private $properties = array(); private $unextendableMethods = array(); /** * @var MethodNode[] */ private $methods = array(); public function getParentClass() { return $this->parentClass; } /** * @param string $class */ public function setParentClass($class) { $this->parentClass = $class ?: 'stdClass'; } /** * @return string[] */ public function getInterfaces() { return $this->interfaces; } /** * @param string $interface */ public function addInterface($interface) { if ($this->hasInterface($interface)) { return; } array_unshift($this->interfaces, $interface); } /** * @param string $interface * * @return bool */ public function hasInterface($interface) { return in_array($interface, $this->interfaces); } public function getProperties() { return $this->properties; } public function addProperty($name, $visibility = 'public') { $visibility = strtolower($visibility); if (!in_array($visibility, array('public', 'private', 'protected'))) { throw new InvalidArgumentException(sprintf( '`%s` property visibility is not supported.', $visibility )); } $this->properties[$name] = $visibility; } /** * @return MethodNode[] */ public function getMethods() { return $this->methods; } public function addMethod(MethodNode $method) { if (!$this->isExtendable($method->getName())){ $message = sprintf( 'Method `%s` is not extendable, so can not be added.', $method->getName() ); throw new MethodNotExtendableException($message, $this->getParentClass(), $method->getName()); } $this->methods[$method->getName()] = $method; } public function removeMethod($name) { unset($this->methods[$name]); } /** * @param string $name * * @return MethodNode|null */ public function getMethod($name) { return $this->hasMethod($name) ? $this->methods[$name] : null; } /** * @param string $name * * @return bool */ public function hasMethod($name) { return isset($this->methods[$name]); } /** * @return string[] */ public function getUnextendableMethods() { return $this->unextendableMethods; } /** * @param string $unextendableMethod */ public function addUnextendableMethod($unextendableMethod) { if (!$this->isExtendable($unextendableMethod)){ return; } $this->unextendableMethods[] = $unextendableMethod; } /** * @param string $method * @return bool */ public function isExtendable($method) { return !in_array($method, $this->unextendableMethods); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\Generator\Node; use Prophecy\Exception\InvalidArgumentException; /** * Method node. * * @author Konstantin Kudryashov */ class MethodNode { private $name; private $code; private $visibility = 'public'; private $static = false; private $returnsReference = false; private $returnType; /** * @var ArgumentNode[] */ private $arguments = array(); /** * @param string $name * @param string $code */ public function __construct($name, $code = null) { $this->name = $name; $this->code = $code; } public function getVisibility() { return $this->visibility; } /** * @param string $visibility */ public function setVisibility($visibility) { $visibility = strtolower($visibility); if (!in_array($visibility, array('public', 'private', 'protected'))) { throw new InvalidArgumentException(sprintf( '`%s` method visibility is not supported.', $visibility )); } $this->visibility = $visibility; } public function isStatic() { return $this->static; } public function setStatic($static = true) { $this->static = (bool) $static; } public function returnsReference() { return $this->returnsReference; } public function setReturnsReference() { $this->returnsReference = true; } public function getName() { return $this->name; } public function addArgument(ArgumentNode $argument) { $this->arguments[] = $argument; } /** * @return ArgumentNode[] */ public function getArguments() { return $this->arguments; } public function hasReturnType() { return null !== $this->returnType; } /** * @param string $type */ public function setReturnType($type = null) { switch ($type) { case '': $this->returnType = null; break; case 'string'; case 'float': case 'int': case 'bool': case 'array': case 'callable': $this->returnType = $type; break; case 'double': case 'real': $this->returnType = 'float'; break; case 'boolean': $this->returnType = 'bool'; break; case 'integer': $this->returnType = 'int'; break; default: $this->returnType = '\\' . ltrim($type, '\\'); } } public function getReturnType() { return $this->returnType; } /** * @param string $code */ public function setCode($code) { $this->code = $code; } public function getCode() { if ($this->returnsReference) { return "throw new \Prophecy\Exception\Doubler\ReturnByReferenceException('Returning by reference not supported', get_class(\$this), '{$this->name}');"; } return (string) $this->code; } public function useParentCode() { $this->code = sprintf( 'return parent::%s(%s);', $this->getName(), implode(', ', array_map(array($this, 'generateArgument'), $this->arguments) ) ); } private function generateArgument(ArgumentNode $arg) { $argument = '$'.$arg->getName(); if ($arg->isVariadic()) { $argument = '...'.$argument; } return $argument; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler\Generator; /** * Reflection interface. * All reflected classes implement this interface. * * @author Konstantin Kudryashov */ interface ReflectionInterface { } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler; use Prophecy\Exception\Doubler\DoubleException; use Prophecy\Exception\Doubler\ClassNotFoundException; use Prophecy\Exception\Doubler\InterfaceNotFoundException; use ReflectionClass; /** * Lazy double. * Gives simple interface to describe double before creating it. * * @author Konstantin Kudryashov */ class LazyDouble { private $doubler; private $class; private $interfaces = array(); private $arguments = null; private $double; /** * Initializes lazy double. * * @param Doubler $doubler */ public function __construct(Doubler $doubler) { $this->doubler = $doubler; } /** * Tells doubler to use specific class as parent one for double. * * @param string|ReflectionClass $class * * @throws \Prophecy\Exception\Doubler\ClassNotFoundException * @throws \Prophecy\Exception\Doubler\DoubleException */ public function setParentClass($class) { if (null !== $this->double) { throw new DoubleException('Can not extend class with already instantiated double.'); } if (!$class instanceof ReflectionClass) { if (!class_exists($class)) { throw new ClassNotFoundException(sprintf('Class %s not found.', $class), $class); } $class = new ReflectionClass($class); } $this->class = $class; } /** * Tells doubler to implement specific interface with double. * * @param string|ReflectionClass $interface * * @throws \Prophecy\Exception\Doubler\InterfaceNotFoundException * @throws \Prophecy\Exception\Doubler\DoubleException */ public function addInterface($interface) { if (null !== $this->double) { throw new DoubleException( 'Can not implement interface with already instantiated double.' ); } if (!$interface instanceof ReflectionClass) { if (!interface_exists($interface)) { throw new InterfaceNotFoundException( sprintf('Interface %s not found.', $interface), $interface ); } $interface = new ReflectionClass($interface); } $this->interfaces[] = $interface; } /** * Sets constructor arguments. * * @param array $arguments */ public function setArguments(array $arguments = null) { $this->arguments = $arguments; } /** * Creates double instance or returns already created one. * * @return DoubleInterface */ public function getInstance() { if (null === $this->double) { if (null !== $this->arguments) { return $this->double = $this->doubler->double( $this->class, $this->interfaces, $this->arguments ); } $this->double = $this->doubler->double($this->class, $this->interfaces); } return $this->double; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Doubler; use ReflectionClass; /** * Name generator. * Generates classname for double. * * @author Konstantin Kudryashov */ class NameGenerator { private static $counter = 1; /** * Generates name. * * @param ReflectionClass $class * @param ReflectionClass[] $interfaces * * @return string */ public function name(ReflectionClass $class = null, array $interfaces) { $parts = array(); if (null !== $class) { $parts[] = $class->getName(); } else { foreach ($interfaces as $interface) { $parts[] = $interface->getShortName(); } } if (!count($parts)) { $parts[] = 'stdClass'; } return sprintf('Double\%s\P%d', implode('\\', $parts), self::$counter++); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Call; use Prophecy\Exception\Prophecy\ObjectProphecyException; use Prophecy\Prophecy\ObjectProphecy; class UnexpectedCallException extends ObjectProphecyException { private $methodName; private $arguments; public function __construct($message, ObjectProphecy $objectProphecy, $methodName, array $arguments) { parent::__construct($message, $objectProphecy); $this->methodName = $methodName; $this->arguments = $arguments; } public function getMethodName() { return $this->methodName; } public function getArguments() { return $this->arguments; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Doubler; use Prophecy\Doubler\Generator\Node\ClassNode; class ClassCreatorException extends \RuntimeException implements DoublerException { private $node; public function __construct($message, ClassNode $node) { parent::__construct($message); $this->node = $node; } public function getClassNode() { return $this->node; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Doubler; use ReflectionClass; class ClassMirrorException extends \RuntimeException implements DoublerException { private $class; public function __construct($message, ReflectionClass $class) { parent::__construct($message); $this->class = $class; } public function getReflectedClass() { return $this->class; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Doubler; class ClassNotFoundException extends DoubleException { private $classname; /** * @param string $message * @param string $classname */ public function __construct($message, $classname) { parent::__construct($message); $this->classname = $classname; } public function getClassname() { return $this->classname; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Doubler; use RuntimeException; class DoubleException extends RuntimeException implements DoublerException { } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Doubler; use Prophecy\Exception\Exception; interface DoublerException extends Exception { } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Doubler; class InterfaceNotFoundException extends ClassNotFoundException { public function getInterfaceName() { return $this->getClassname(); } } methodName = $methodName; $this->className = $className; } /** * @return string */ public function getMethodName() { return $this->methodName; } /** * @return string */ public function getClassName() { return $this->className; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Doubler; class MethodNotFoundException extends DoubleException { /** * @var string */ private $classname; /** * @var string */ private $methodName; /** * @var array */ private $arguments; /** * @param string $message * @param string $classname * @param string $methodName * @param null|Argument\ArgumentsWildcard|array $arguments */ public function __construct($message, $classname, $methodName, $arguments = null) { parent::__construct($message); $this->classname = $classname; $this->methodName = $methodName; $this->arguments = $arguments; } public function getClassname() { return $this->classname; } public function getMethodName() { return $this->methodName; } public function getArguments() { return $this->arguments; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Doubler; class ReturnByReferenceException extends DoubleException { private $classname; private $methodName; /** * @param string $message * @param string $classname * @param string $methodName */ public function __construct($message, $classname, $methodName) { parent::__construct($message); $this->classname = $classname; $this->methodName = $methodName; } public function getClassname() { return $this->classname; } public function getMethodName() { return $this->methodName; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception; /** * Core Prophecy exception interface. * All Prophecy exceptions implement it. * * @author Konstantin Kudryashov */ interface Exception { /** * @return string */ public function getMessage(); } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception; class InvalidArgumentException extends \InvalidArgumentException implements Exception { } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Prediction; use Prophecy\Prophecy\ObjectProphecy; class AggregateException extends \RuntimeException implements PredictionException { private $exceptions = array(); private $objectProphecy; public function append(PredictionException $exception) { $message = $exception->getMessage(); $message = ' '.strtr($message, array("\n" => "\n "))."\n"; $this->message = rtrim($this->message.$message); $this->exceptions[] = $exception; } /** * @return PredictionException[] */ public function getExceptions() { return $this->exceptions; } public function setObjectProphecy(ObjectProphecy $objectProphecy) { $this->objectProphecy = $objectProphecy; } /** * @return ObjectProphecy */ public function getObjectProphecy() { return $this->objectProphecy; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Prediction; use RuntimeException; /** * Basic failed prediction exception. * Use it for custom prediction failures. * * @author Konstantin Kudryashov */ class FailedPredictionException extends RuntimeException implements PredictionException { } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Prediction; use Prophecy\Exception\Prophecy\MethodProphecyException; class NoCallsException extends MethodProphecyException implements PredictionException { } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Prediction; use Prophecy\Exception\Exception; interface PredictionException extends Exception { } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Prediction; use Prophecy\Prophecy\MethodProphecy; class UnexpectedCallsCountException extends UnexpectedCallsException { private $expectedCount; public function __construct($message, MethodProphecy $methodProphecy, $count, array $calls) { parent::__construct($message, $methodProphecy, $calls); $this->expectedCount = intval($count); } public function getExpectedCount() { return $this->expectedCount; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Prediction; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Exception\Prophecy\MethodProphecyException; class UnexpectedCallsException extends MethodProphecyException implements PredictionException { private $calls = array(); public function __construct($message, MethodProphecy $methodProphecy, array $calls) { parent::__construct($message, $methodProphecy); $this->calls = $calls; } public function getCalls() { return $this->calls; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Prophecy; use Prophecy\Prophecy\MethodProphecy; class MethodProphecyException extends ObjectProphecyException { private $methodProphecy; public function __construct($message, MethodProphecy $methodProphecy) { parent::__construct($message, $methodProphecy->getObjectProphecy()); $this->methodProphecy = $methodProphecy; } /** * @return MethodProphecy */ public function getMethodProphecy() { return $this->methodProphecy; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Prophecy; use Prophecy\Prophecy\ObjectProphecy; class ObjectProphecyException extends \RuntimeException implements ProphecyException { private $objectProphecy; public function __construct($message, ObjectProphecy $objectProphecy) { parent::__construct($message); $this->objectProphecy = $objectProphecy; } /** * @return ObjectProphecy */ public function getObjectProphecy() { return $this->objectProphecy; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Exception\Prophecy; use Prophecy\Exception\Exception; interface ProphecyException extends Exception { } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\PhpDocumentor; use phpDocumentor\Reflection\DocBlock\Tag\MethodTag as LegacyMethodTag; use phpDocumentor\Reflection\DocBlock\Tags\Method; /** * @author Théo FIDRY * * @internal */ final class ClassAndInterfaceTagRetriever implements MethodTagRetrieverInterface { private $classRetriever; public function __construct(MethodTagRetrieverInterface $classRetriever = null) { if (null !== $classRetriever) { $this->classRetriever = $classRetriever; return; } $this->classRetriever = class_exists('phpDocumentor\Reflection\DocBlockFactory') && class_exists('phpDocumentor\Reflection\Types\ContextFactory') ? new ClassTagRetriever() : new LegacyClassTagRetriever() ; } /** * @param \ReflectionClass $reflectionClass * * @return LegacyMethodTag[]|Method[] */ public function getTagList(\ReflectionClass $reflectionClass) { return array_merge( $this->classRetriever->getTagList($reflectionClass), $this->getInterfacesTagList($reflectionClass) ); } /** * @param \ReflectionClass $reflectionClass * * @return LegacyMethodTag[]|Method[] */ private function getInterfacesTagList(\ReflectionClass $reflectionClass) { $interfaces = $reflectionClass->getInterfaces(); $tagList = array(); foreach($interfaces as $interface) { $tagList = array_merge($tagList, $this->classRetriever->getTagList($interface)); } return $tagList; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\PhpDocumentor; use phpDocumentor\Reflection\DocBlock\Tags\Method; use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\Types\ContextFactory; /** * @author Théo FIDRY * * @internal */ final class ClassTagRetriever implements MethodTagRetrieverInterface { private $docBlockFactory; private $contextFactory; public function __construct() { $this->docBlockFactory = DocBlockFactory::createInstance(); $this->contextFactory = new ContextFactory(); } /** * @param \ReflectionClass $reflectionClass * * @return Method[] */ public function getTagList(\ReflectionClass $reflectionClass) { try { $phpdoc = $this->docBlockFactory->create( $reflectionClass, $this->contextFactory->createFromReflector($reflectionClass) ); return $phpdoc->getTagsByName('method'); } catch (\InvalidArgumentException $e) { return array(); } } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\PhpDocumentor; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock\Tag\MethodTag as LegacyMethodTag; /** * @author Théo FIDRY * * @internal */ final class LegacyClassTagRetriever implements MethodTagRetrieverInterface { /** * @param \ReflectionClass $reflectionClass * * @return LegacyMethodTag[] */ public function getTagList(\ReflectionClass $reflectionClass) { $phpdoc = new DocBlock($reflectionClass->getDocComment()); return $phpdoc->getTagsByName('method'); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\PhpDocumentor; use phpDocumentor\Reflection\DocBlock\Tag\MethodTag as LegacyMethodTag; use phpDocumentor\Reflection\DocBlock\Tags\Method; /** * @author Théo FIDRY * * @internal */ interface MethodTagRetrieverInterface { /** * @param \ReflectionClass $reflectionClass * * @return LegacyMethodTag[]|Method[] */ public function getTagList(\ReflectionClass $reflectionClass); } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prediction; use Prophecy\Call\Call; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Exception\InvalidArgumentException; use Closure; /** * Callback prediction. * * @author Konstantin Kudryashov */ class CallbackPrediction implements PredictionInterface { private $callback; /** * Initializes callback prediction. * * @param callable $callback Custom callback * * @throws \Prophecy\Exception\InvalidArgumentException */ public function __construct($callback) { if (!is_callable($callback)) { throw new InvalidArgumentException(sprintf( 'Callable expected as an argument to CallbackPrediction, but got %s.', gettype($callback) )); } $this->callback = $callback; } /** * Executes preset callback. * * @param Call[] $calls * @param ObjectProphecy $object * @param MethodProphecy $method */ public function check(array $calls, ObjectProphecy $object, MethodProphecy $method) { $callback = $this->callback; if ($callback instanceof Closure && method_exists('Closure', 'bind')) { $callback = Closure::bind($callback, $object); } call_user_func($callback, $calls, $object, $method); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prediction; use Prophecy\Call\Call; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Argument\ArgumentsWildcard; use Prophecy\Argument\Token\AnyValuesToken; use Prophecy\Util\StringUtil; use Prophecy\Exception\Prediction\NoCallsException; /** * Call prediction. * * @author Konstantin Kudryashov */ class CallPrediction implements PredictionInterface { private $util; /** * Initializes prediction. * * @param StringUtil $util */ public function __construct(StringUtil $util = null) { $this->util = $util ?: new StringUtil; } /** * Tests that there was at least one call. * * @param Call[] $calls * @param ObjectProphecy $object * @param MethodProphecy $method * * @throws \Prophecy\Exception\Prediction\NoCallsException */ public function check(array $calls, ObjectProphecy $object, MethodProphecy $method) { if (count($calls)) { return; } $methodCalls = $object->findProphecyMethodCalls( $method->getMethodName(), new ArgumentsWildcard(array(new AnyValuesToken)) ); if (count($methodCalls)) { throw new NoCallsException(sprintf( "No calls have been made that match:\n". " %s->%s(%s)\n". "but expected at least one.\n". "Recorded `%s(...)` calls:\n%s", get_class($object->reveal()), $method->getMethodName(), $method->getArgumentsWildcard(), $method->getMethodName(), $this->util->stringifyCalls($methodCalls) ), $method); } throw new NoCallsException(sprintf( "No calls have been made that match:\n". " %s->%s(%s)\n". "but expected at least one.", get_class($object->reveal()), $method->getMethodName(), $method->getArgumentsWildcard() ), $method); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prediction; use Prophecy\Call\Call; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Argument\ArgumentsWildcard; use Prophecy\Argument\Token\AnyValuesToken; use Prophecy\Util\StringUtil; use Prophecy\Exception\Prediction\UnexpectedCallsCountException; /** * Prediction interface. * Predictions are logical test blocks, tied to `should...` keyword. * * @author Konstantin Kudryashov */ class CallTimesPrediction implements PredictionInterface { private $times; private $util; /** * Initializes prediction. * * @param int $times * @param StringUtil $util */ public function __construct($times, StringUtil $util = null) { $this->times = intval($times); $this->util = $util ?: new StringUtil; } /** * Tests that there was exact amount of calls made. * * @param Call[] $calls * @param ObjectProphecy $object * @param MethodProphecy $method * * @throws \Prophecy\Exception\Prediction\UnexpectedCallsCountException */ public function check(array $calls, ObjectProphecy $object, MethodProphecy $method) { if ($this->times == count($calls)) { return; } $methodCalls = $object->findProphecyMethodCalls( $method->getMethodName(), new ArgumentsWildcard(array(new AnyValuesToken)) ); if (count($calls)) { $message = sprintf( "Expected exactly %d calls that match:\n". " %s->%s(%s)\n". "but %d were made:\n%s", $this->times, get_class($object->reveal()), $method->getMethodName(), $method->getArgumentsWildcard(), count($calls), $this->util->stringifyCalls($calls) ); } elseif (count($methodCalls)) { $message = sprintf( "Expected exactly %d calls that match:\n". " %s->%s(%s)\n". "but none were made.\n". "Recorded `%s(...)` calls:\n%s", $this->times, get_class($object->reveal()), $method->getMethodName(), $method->getArgumentsWildcard(), $method->getMethodName(), $this->util->stringifyCalls($methodCalls) ); } else { $message = sprintf( "Expected exactly %d calls that match:\n". " %s->%s(%s)\n". "but none were made.", $this->times, get_class($object->reveal()), $method->getMethodName(), $method->getArgumentsWildcard() ); } throw new UnexpectedCallsCountException($message, $method, $this->times, $calls); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prediction; use Prophecy\Call\Call; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Util\StringUtil; use Prophecy\Exception\Prediction\UnexpectedCallsException; /** * No calls prediction. * * @author Konstantin Kudryashov */ class NoCallsPrediction implements PredictionInterface { private $util; /** * Initializes prediction. * * @param null|StringUtil $util */ public function __construct(StringUtil $util = null) { $this->util = $util ?: new StringUtil; } /** * Tests that there were no calls made. * * @param Call[] $calls * @param ObjectProphecy $object * @param MethodProphecy $method * * @throws \Prophecy\Exception\Prediction\UnexpectedCallsException */ public function check(array $calls, ObjectProphecy $object, MethodProphecy $method) { if (!count($calls)) { return; } $verb = count($calls) === 1 ? 'was' : 'were'; throw new UnexpectedCallsException(sprintf( "No calls expected that match:\n". " %s->%s(%s)\n". "but %d %s made:\n%s", get_class($object->reveal()), $method->getMethodName(), $method->getArgumentsWildcard(), count($calls), $verb, $this->util->stringifyCalls($calls) ), $method, $calls); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prediction; use Prophecy\Call\Call; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\MethodProphecy; /** * Prediction interface. * Predictions are logical test blocks, tied to `should...` keyword. * * @author Konstantin Kudryashov */ interface PredictionInterface { /** * Tests that double fulfilled prediction. * * @param Call[] $calls * @param ObjectProphecy $object * @param MethodProphecy $method * * @throws object * @return void */ public function check(array $calls, ObjectProphecy $object, MethodProphecy $method); } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Promise; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Exception\InvalidArgumentException; use Closure; /** * Callback promise. * * @author Konstantin Kudryashov */ class CallbackPromise implements PromiseInterface { private $callback; /** * Initializes callback promise. * * @param callable $callback Custom callback * * @throws \Prophecy\Exception\InvalidArgumentException */ public function __construct($callback) { if (!is_callable($callback)) { throw new InvalidArgumentException(sprintf( 'Callable expected as an argument to CallbackPromise, but got %s.', gettype($callback) )); } $this->callback = $callback; } /** * Evaluates promise callback. * * @param array $args * @param ObjectProphecy $object * @param MethodProphecy $method * * @return mixed */ public function execute(array $args, ObjectProphecy $object, MethodProphecy $method) { $callback = $this->callback; if ($callback instanceof Closure && method_exists('Closure', 'bind')) { $callback = Closure::bind($callback, $object); } return call_user_func($callback, $args, $object, $method); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Promise; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\MethodProphecy; /** * Promise interface. * Promises are logical blocks, tied to `will...` keyword. * * @author Konstantin Kudryashov */ interface PromiseInterface { /** * Evaluates promise. * * @param array $args * @param ObjectProphecy $object * @param MethodProphecy $method * * @return mixed */ public function execute(array $args, ObjectProphecy $object, MethodProphecy $method); } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Promise; use Prophecy\Exception\InvalidArgumentException; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\MethodProphecy; /** * Return argument promise. * * @author Konstantin Kudryashov */ class ReturnArgumentPromise implements PromiseInterface { /** * @var int */ private $index; /** * Initializes callback promise. * * @param int $index The zero-indexed number of the argument to return * * @throws \Prophecy\Exception\InvalidArgumentException */ public function __construct($index = 0) { if (!is_int($index) || $index < 0) { throw new InvalidArgumentException(sprintf( 'Zero-based index expected as argument to ReturnArgumentPromise, but got %s.', $index )); } $this->index = $index; } /** * Returns nth argument if has one, null otherwise. * * @param array $args * @param ObjectProphecy $object * @param MethodProphecy $method * * @return null|mixed */ public function execute(array $args, ObjectProphecy $object, MethodProphecy $method) { return count($args) > $this->index ? $args[$this->index] : null; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Promise; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\MethodProphecy; /** * Return promise. * * @author Konstantin Kudryashov */ class ReturnPromise implements PromiseInterface { private $returnValues = array(); /** * Initializes promise. * * @param array $returnValues Array of values */ public function __construct(array $returnValues) { $this->returnValues = $returnValues; } /** * Returns saved values one by one until last one, then continuously returns last value. * * @param array $args * @param ObjectProphecy $object * @param MethodProphecy $method * * @return mixed */ public function execute(array $args, ObjectProphecy $object, MethodProphecy $method) { $value = array_shift($this->returnValues); if (!count($this->returnValues)) { $this->returnValues[] = $value; } return $value; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Promise; use Doctrine\Instantiator\Instantiator; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\MethodProphecy; use Prophecy\Exception\InvalidArgumentException; use ReflectionClass; /** * Throw promise. * * @author Konstantin Kudryashov */ class ThrowPromise implements PromiseInterface { private $exception; /** * @var \Doctrine\Instantiator\Instantiator */ private $instantiator; /** * Initializes promise. * * @param string|\Exception $exception Exception class name or instance * * @throws \Prophecy\Exception\InvalidArgumentException */ public function __construct($exception) { if (is_string($exception)) { if (!class_exists($exception) && 'Exception' !== $exception && !is_subclass_of($exception, 'Exception')) { throw new InvalidArgumentException(sprintf( 'Exception class or instance expected as argument to ThrowPromise, but got %s.', $exception )); } } elseif (!$exception instanceof \Exception) { throw new InvalidArgumentException(sprintf( 'Exception class or instance expected as argument to ThrowPromise, but got %s.', is_object($exception) ? get_class($exception) : gettype($exception) )); } $this->exception = $exception; } /** * Throws predefined exception. * * @param array $args * @param ObjectProphecy $object * @param MethodProphecy $method * * @throws object */ public function execute(array $args, ObjectProphecy $object, MethodProphecy $method) { if (is_string($this->exception)) { $classname = $this->exception; $reflection = new ReflectionClass($classname); $constructor = $reflection->getConstructor(); if ($constructor->isPublic() && 0 == $constructor->getNumberOfRequiredParameters()) { throw $reflection->newInstance(); } if (!$this->instantiator) { $this->instantiator = new Instantiator(); } throw $this->instantiator->instantiate($classname); } throw $this->exception; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prophecy; use Prophecy\Argument; use Prophecy\Prophet; use Prophecy\Promise; use Prophecy\Prediction; use Prophecy\Exception\Doubler\MethodNotFoundException; use Prophecy\Exception\InvalidArgumentException; use Prophecy\Exception\Prophecy\MethodProphecyException; /** * Method prophecy. * * @author Konstantin Kudryashov */ class MethodProphecy { private $objectProphecy; private $methodName; private $argumentsWildcard; private $promise; private $prediction; private $checkedPredictions = array(); private $bound = false; /** * Initializes method prophecy. * * @param ObjectProphecy $objectProphecy * @param string $methodName * @param null|Argument\ArgumentsWildcard|array $arguments * * @throws \Prophecy\Exception\Doubler\MethodNotFoundException If method not found */ public function __construct(ObjectProphecy $objectProphecy, $methodName, $arguments = null) { $double = $objectProphecy->reveal(); if (!method_exists($double, $methodName)) { throw new MethodNotFoundException(sprintf( 'Method `%s::%s()` is not defined.', get_class($double), $methodName ), get_class($double), $methodName, $arguments); } $this->objectProphecy = $objectProphecy; $this->methodName = $methodName; $reflectedMethod = new \ReflectionMethod($double, $methodName); if ($reflectedMethod->isFinal()) { throw new MethodProphecyException(sprintf( "Can not add prophecy for a method `%s::%s()`\n". "as it is a final method.", get_class($double), $methodName ), $this); } if (null !== $arguments) { $this->withArguments($arguments); } if (version_compare(PHP_VERSION, '7.0', '>=') && true === $reflectedMethod->hasReturnType()) { $type = (string) $reflectedMethod->getReturnType(); $this->will(function () use ($type) { switch ($type) { case 'string': return ''; case 'float': return 0.0; case 'int': return 0; case 'bool': return false; case 'array': return array(); case 'callable': case 'Closure': return function () {}; case 'Traversable': case 'Generator': // Remove eval() when minimum version >=5.5 /** @var callable $generator */ $generator = eval('return function () { yield; };'); return $generator(); default: $prophet = new Prophet; return $prophet->prophesize($type)->reveal(); } }); } } /** * Sets argument wildcard. * * @param array|Argument\ArgumentsWildcard $arguments * * @return $this * * @throws \Prophecy\Exception\InvalidArgumentException */ public function withArguments($arguments) { if (is_array($arguments)) { $arguments = new Argument\ArgumentsWildcard($arguments); } if (!$arguments instanceof Argument\ArgumentsWildcard) { throw new InvalidArgumentException(sprintf( "Either an array or an instance of ArgumentsWildcard expected as\n". 'a `MethodProphecy::withArguments()` argument, but got %s.', gettype($arguments) )); } $this->argumentsWildcard = $arguments; return $this; } /** * Sets custom promise to the prophecy. * * @param callable|Promise\PromiseInterface $promise * * @return $this * * @throws \Prophecy\Exception\InvalidArgumentException */ public function will($promise) { if (is_callable($promise)) { $promise = new Promise\CallbackPromise($promise); } if (!$promise instanceof Promise\PromiseInterface) { throw new InvalidArgumentException(sprintf( 'Expected callable or instance of PromiseInterface, but got %s.', gettype($promise) )); } $this->bindToObjectProphecy(); $this->promise = $promise; return $this; } /** * Sets return promise to the prophecy. * * @see Prophecy\Promise\ReturnPromise * * @return $this */ public function willReturn() { return $this->will(new Promise\ReturnPromise(func_get_args())); } /** * Sets return argument promise to the prophecy. * * @param int $index The zero-indexed number of the argument to return * * @see Prophecy\Promise\ReturnArgumentPromise * * @return $this */ public function willReturnArgument($index = 0) { return $this->will(new Promise\ReturnArgumentPromise($index)); } /** * Sets throw promise to the prophecy. * * @see Prophecy\Promise\ThrowPromise * * @param string|\Exception $exception Exception class or instance * * @return $this */ public function willThrow($exception) { return $this->will(new Promise\ThrowPromise($exception)); } /** * Sets custom prediction to the prophecy. * * @param callable|Prediction\PredictionInterface $prediction * * @return $this * * @throws \Prophecy\Exception\InvalidArgumentException */ public function should($prediction) { if (is_callable($prediction)) { $prediction = new Prediction\CallbackPrediction($prediction); } if (!$prediction instanceof Prediction\PredictionInterface) { throw new InvalidArgumentException(sprintf( 'Expected callable or instance of PredictionInterface, but got %s.', gettype($prediction) )); } $this->bindToObjectProphecy(); $this->prediction = $prediction; return $this; } /** * Sets call prediction to the prophecy. * * @see Prophecy\Prediction\CallPrediction * * @return $this */ public function shouldBeCalled() { return $this->should(new Prediction\CallPrediction); } /** * Sets no calls prediction to the prophecy. * * @see Prophecy\Prediction\NoCallsPrediction * * @return $this */ public function shouldNotBeCalled() { return $this->should(new Prediction\NoCallsPrediction); } /** * Sets call times prediction to the prophecy. * * @see Prophecy\Prediction\CallTimesPrediction * * @param $count * * @return $this */ public function shouldBeCalledTimes($count) { return $this->should(new Prediction\CallTimesPrediction($count)); } /** * Checks provided prediction immediately. * * @param callable|Prediction\PredictionInterface $prediction * * @return $this * * @throws \Prophecy\Exception\InvalidArgumentException */ public function shouldHave($prediction) { if (is_callable($prediction)) { $prediction = new Prediction\CallbackPrediction($prediction); } if (!$prediction instanceof Prediction\PredictionInterface) { throw new InvalidArgumentException(sprintf( 'Expected callable or instance of PredictionInterface, but got %s.', gettype($prediction) )); } if (null === $this->promise) { $this->willReturn(); } $calls = $this->getObjectProphecy()->findProphecyMethodCalls( $this->getMethodName(), $this->getArgumentsWildcard() ); try { $prediction->check($calls, $this->getObjectProphecy(), $this); $this->checkedPredictions[] = $prediction; } catch (\Exception $e) { $this->checkedPredictions[] = $prediction; throw $e; } return $this; } /** * Checks call prediction. * * @see Prophecy\Prediction\CallPrediction * * @return $this */ public function shouldHaveBeenCalled() { return $this->shouldHave(new Prediction\CallPrediction); } /** * Checks no calls prediction. * * @see Prophecy\Prediction\NoCallsPrediction * * @return $this */ public function shouldNotHaveBeenCalled() { return $this->shouldHave(new Prediction\NoCallsPrediction); } /** * Checks no calls prediction. * * @see Prophecy\Prediction\NoCallsPrediction * @deprecated * * @return $this */ public function shouldNotBeenCalled() { return $this->shouldNotHaveBeenCalled(); } /** * Checks call times prediction. * * @see Prophecy\Prediction\CallTimesPrediction * * @param int $count * * @return $this */ public function shouldHaveBeenCalledTimes($count) { return $this->shouldHave(new Prediction\CallTimesPrediction($count)); } /** * Checks currently registered [with should(...)] prediction. */ public function checkPrediction() { if (null === $this->prediction) { return; } $this->shouldHave($this->prediction); } /** * Returns currently registered promise. * * @return null|Promise\PromiseInterface */ public function getPromise() { return $this->promise; } /** * Returns currently registered prediction. * * @return null|Prediction\PredictionInterface */ public function getPrediction() { return $this->prediction; } /** * Returns predictions that were checked on this object. * * @return Prediction\PredictionInterface[] */ public function getCheckedPredictions() { return $this->checkedPredictions; } /** * Returns object prophecy this method prophecy is tied to. * * @return ObjectProphecy */ public function getObjectProphecy() { return $this->objectProphecy; } /** * Returns method name. * * @return string */ public function getMethodName() { return $this->methodName; } /** * Returns arguments wildcard. * * @return Argument\ArgumentsWildcard */ public function getArgumentsWildcard() { return $this->argumentsWildcard; } private function bindToObjectProphecy() { if ($this->bound) { return; } $this->getObjectProphecy()->addMethodProphecy($this); $this->bound = true; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prophecy; use SebastianBergmann\Comparator\ComparisonFailure; use Prophecy\Comparator\Factory as ComparatorFactory; use Prophecy\Call\Call; use Prophecy\Doubler\LazyDouble; use Prophecy\Argument\ArgumentsWildcard; use Prophecy\Call\CallCenter; use Prophecy\Exception\Prophecy\ObjectProphecyException; use Prophecy\Exception\Prophecy\MethodProphecyException; use Prophecy\Exception\Prediction\AggregateException; use Prophecy\Exception\Prediction\PredictionException; /** * Object prophecy. * * @author Konstantin Kudryashov */ class ObjectProphecy implements ProphecyInterface { private $lazyDouble; private $callCenter; private $revealer; private $comparatorFactory; /** * @var MethodProphecy[][] */ private $methodProphecies = array(); /** * Initializes object prophecy. * * @param LazyDouble $lazyDouble * @param CallCenter $callCenter * @param RevealerInterface $revealer * @param ComparatorFactory $comparatorFactory */ public function __construct( LazyDouble $lazyDouble, CallCenter $callCenter = null, RevealerInterface $revealer = null, ComparatorFactory $comparatorFactory = null ) { $this->lazyDouble = $lazyDouble; $this->callCenter = $callCenter ?: new CallCenter; $this->revealer = $revealer ?: new Revealer; $this->comparatorFactory = $comparatorFactory ?: ComparatorFactory::getInstance(); } /** * Forces double to extend specific class. * * @param string $class * * @return $this */ public function willExtend($class) { $this->lazyDouble->setParentClass($class); return $this; } /** * Forces double to implement specific interface. * * @param string $interface * * @return $this */ public function willImplement($interface) { $this->lazyDouble->addInterface($interface); return $this; } /** * Sets constructor arguments. * * @param array $arguments * * @return $this */ public function willBeConstructedWith(array $arguments = null) { $this->lazyDouble->setArguments($arguments); return $this; } /** * Reveals double. * * @return object * * @throws \Prophecy\Exception\Prophecy\ObjectProphecyException If double doesn't implement needed interface */ public function reveal() { $double = $this->lazyDouble->getInstance(); if (null === $double || !$double instanceof ProphecySubjectInterface) { throw new ObjectProphecyException( "Generated double must implement ProphecySubjectInterface, but it does not.\n". 'It seems you have wrongly configured doubler without required ClassPatch.', $this ); } $double->setProphecy($this); return $double; } /** * Adds method prophecy to object prophecy. * * @param MethodProphecy $methodProphecy * * @throws \Prophecy\Exception\Prophecy\MethodProphecyException If method prophecy doesn't * have arguments wildcard */ public function addMethodProphecy(MethodProphecy $methodProphecy) { $argumentsWildcard = $methodProphecy->getArgumentsWildcard(); if (null === $argumentsWildcard) { throw new MethodProphecyException(sprintf( "Can not add prophecy for a method `%s::%s()`\n". "as you did not specify arguments wildcard for it.", get_class($this->reveal()), $methodProphecy->getMethodName() ), $methodProphecy); } $methodName = $methodProphecy->getMethodName(); if (!isset($this->methodProphecies[$methodName])) { $this->methodProphecies[$methodName] = array(); } $this->methodProphecies[$methodName][] = $methodProphecy; } /** * Returns either all or related to single method prophecies. * * @param null|string $methodName * * @return MethodProphecy[] */ public function getMethodProphecies($methodName = null) { if (null === $methodName) { return $this->methodProphecies; } if (!isset($this->methodProphecies[$methodName])) { return array(); } return $this->methodProphecies[$methodName]; } /** * Makes specific method call. * * @param string $methodName * @param array $arguments * * @return mixed */ public function makeProphecyMethodCall($methodName, array $arguments) { $arguments = $this->revealer->reveal($arguments); $return = $this->callCenter->makeCall($this, $methodName, $arguments); return $this->revealer->reveal($return); } /** * Finds calls by method name & arguments wildcard. * * @param string $methodName * @param ArgumentsWildcard $wildcard * * @return Call[] */ public function findProphecyMethodCalls($methodName, ArgumentsWildcard $wildcard) { return $this->callCenter->findCalls($methodName, $wildcard); } /** * Checks that registered method predictions do not fail. * * @throws \Prophecy\Exception\Prediction\AggregateException If any of registered predictions fail */ public function checkProphecyMethodsPredictions() { $exception = new AggregateException(sprintf("%s:\n", get_class($this->reveal()))); $exception->setObjectProphecy($this); foreach ($this->methodProphecies as $prophecies) { foreach ($prophecies as $prophecy) { try { $prophecy->checkPrediction(); } catch (PredictionException $e) { $exception->append($e); } } } if (count($exception->getExceptions())) { throw $exception; } } /** * Creates new method prophecy using specified method name and arguments. * * @param string $methodName * @param array $arguments * * @return MethodProphecy */ public function __call($methodName, array $arguments) { $arguments = new ArgumentsWildcard($this->revealer->reveal($arguments)); foreach ($this->getMethodProphecies($methodName) as $prophecy) { $argumentsWildcard = $prophecy->getArgumentsWildcard(); $comparator = $this->comparatorFactory->getComparatorFor( $argumentsWildcard, $arguments ); try { $comparator->assertEquals($argumentsWildcard, $arguments); return $prophecy; } catch (ComparisonFailure $failure) {} } return new MethodProphecy($this, $methodName, $arguments); } /** * Tries to get property value from double. * * @param string $name * * @return mixed */ public function __get($name) { return $this->reveal()->$name; } /** * Tries to set property value to double. * * @param string $name * @param mixed $value */ public function __set($name, $value) { $this->reveal()->$name = $this->revealer->reveal($value); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prophecy; /** * Core Prophecy interface. * * @author Konstantin Kudryashov */ interface ProphecyInterface { /** * Reveals prophecy object (double) . * * @return object */ public function reveal(); } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prophecy; /** * Controllable doubles interface. * * @author Konstantin Kudryashov */ interface ProphecySubjectInterface { /** * Sets subject prophecy. * * @param ProphecyInterface $prophecy */ public function setProphecy(ProphecyInterface $prophecy); /** * Returns subject prophecy. * * @return ProphecyInterface */ public function getProphecy(); } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prophecy; /** * Basic prophecies revealer. * * @author Konstantin Kudryashov */ class Revealer implements RevealerInterface { /** * Unwraps value(s). * * @param mixed $value * * @return mixed */ public function reveal($value) { if (is_array($value)) { return array_map(array($this, __FUNCTION__), $value); } if (!is_object($value)) { return $value; } if ($value instanceof ProphecyInterface) { $value = $value->reveal(); } return $value; } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Prophecy; /** * Prophecies revealer interface. * * @author Konstantin Kudryashov */ interface RevealerInterface { /** * Unwraps value(s). * * @param mixed $value * * @return mixed */ public function reveal($value); } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy; use Prophecy\Doubler\Doubler; use Prophecy\Doubler\LazyDouble; use Prophecy\Doubler\ClassPatch; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\RevealerInterface; use Prophecy\Prophecy\Revealer; use Prophecy\Call\CallCenter; use Prophecy\Util\StringUtil; use Prophecy\Exception\Prediction\PredictionException; use Prophecy\Exception\Prediction\AggregateException; /** * Prophet creates prophecies. * * @author Konstantin Kudryashov */ class Prophet { private $doubler; private $revealer; private $util; /** * @var ObjectProphecy[] */ private $prophecies = array(); /** * Initializes Prophet. * * @param null|Doubler $doubler * @param null|RevealerInterface $revealer * @param null|StringUtil $util */ public function __construct(Doubler $doubler = null, RevealerInterface $revealer = null, StringUtil $util = null) { if (null === $doubler) { $doubler = new Doubler; $doubler->registerClassPatch(new ClassPatch\SplFileInfoPatch); $doubler->registerClassPatch(new ClassPatch\TraversablePatch); $doubler->registerClassPatch(new ClassPatch\DisableConstructorPatch); $doubler->registerClassPatch(new ClassPatch\ProphecySubjectPatch); $doubler->registerClassPatch(new ClassPatch\ReflectionClassNewInstancePatch); $doubler->registerClassPatch(new ClassPatch\HhvmExceptionPatch()); $doubler->registerClassPatch(new ClassPatch\MagicCallPatch); $doubler->registerClassPatch(new ClassPatch\KeywordPatch); } $this->doubler = $doubler; $this->revealer = $revealer ?: new Revealer; $this->util = $util ?: new StringUtil; } /** * Creates new object prophecy. * * @param null|string $classOrInterface Class or interface name * * @return ObjectProphecy */ public function prophesize($classOrInterface = null) { $this->prophecies[] = $prophecy = new ObjectProphecy( new LazyDouble($this->doubler), new CallCenter($this->util), $this->revealer ); if ($classOrInterface && class_exists($classOrInterface)) { return $prophecy->willExtend($classOrInterface); } if ($classOrInterface && interface_exists($classOrInterface)) { return $prophecy->willImplement($classOrInterface); } return $prophecy; } /** * Returns all created object prophecies. * * @return ObjectProphecy[] */ public function getProphecies() { return $this->prophecies; } /** * Returns Doubler instance assigned to this Prophet. * * @return Doubler */ public function getDoubler() { return $this->doubler; } /** * Checks all predictions defined by prophecies of this Prophet. * * @throws Exception\Prediction\AggregateException If any prediction fails */ public function checkPredictions() { $exception = new AggregateException("Some predictions failed:\n"); foreach ($this->prophecies as $prophecy) { try { $prophecy->checkProphecyMethodsPredictions(); } catch (PredictionException $e) { $exception->append($e); } } if (count($exception->getExceptions())) { throw $exception; } } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * This class is a modification from sebastianbergmann/exporter * @see https://github.com/sebastianbergmann/exporter */ class ExportUtil { /** * Exports a value as a string * * The output of this method is similar to the output of print_r(), but * improved in various aspects: * * - NULL is rendered as "null" (instead of "") * - TRUE is rendered as "true" (instead of "1") * - FALSE is rendered as "false" (instead of "") * - Strings are always quoted with single quotes * - Carriage returns and newlines are normalized to \n * - Recursion and repeated rendering is treated properly * * @param mixed $value * @param int $indentation The indentation level of the 2nd+ line * @return string */ public static function export($value, $indentation = 0) { return self::recursiveExport($value, $indentation); } /** * Converts an object to an array containing all of its private, protected * and public properties. * * @param mixed $value * @return array */ public static function toArray($value) { if (!is_object($value)) { return (array) $value; } $array = array(); foreach ((array) $value as $key => $val) { // properties are transformed to keys in the following way: // private $property => "\0Classname\0property" // protected $property => "\0*\0property" // public $property => "property" if (preg_match('/^\0.+\0(.+)$/', $key, $matches)) { $key = $matches[1]; } // See https://github.com/php/php-src/commit/5721132 if ($key === "\0gcdata") { continue; } $array[$key] = $val; } // Some internal classes like SplObjectStorage don't work with the // above (fast) mechanism nor with reflection in Zend. // Format the output similarly to print_r() in this case if ($value instanceof \SplObjectStorage) { // However, the fast method does work in HHVM, and exposes the // internal implementation. Hide it again. if (property_exists('\SplObjectStorage', '__storage')) { unset($array['__storage']); } elseif (property_exists('\SplObjectStorage', 'storage')) { unset($array['storage']); } if (property_exists('\SplObjectStorage', '__key')) { unset($array['__key']); } foreach ($value as $key => $val) { $array[spl_object_hash($val)] = array( 'obj' => $val, 'inf' => $value->getInfo(), ); } } return $array; } /** * Recursive implementation of export * * @param mixed $value The value to export * @param int $indentation The indentation level of the 2nd+ line * @param \SebastianBergmann\RecursionContext\Context $processed Previously processed objects * @return string * @see SebastianBergmann\Exporter\Exporter::export */ protected static function recursiveExport(&$value, $indentation, $processed = null) { if ($value === null) { return 'null'; } if ($value === true) { return 'true'; } if ($value === false) { return 'false'; } if (is_float($value) && floatval(intval($value)) === $value) { return "$value.0"; } if (is_resource($value)) { return sprintf( 'resource(%d) of type (%s)', $value, get_resource_type($value) ); } if (is_string($value)) { // Match for most non printable chars somewhat taking multibyte chars into account if (preg_match('/[^\x09-\x0d\x20-\xff]/', $value)) { return 'Binary String: 0x' . bin2hex($value); } return "'" . str_replace(array("\r\n", "\n\r", "\r"), array("\n", "\n", "\n"), $value) . "'"; } $whitespace = str_repeat(' ', 4 * $indentation); if (!$processed) { $processed = new Context; } if (is_array($value)) { if (($key = $processed->contains($value)) !== false) { return 'Array &' . $key; } $array = $value; $key = $processed->add($value); $values = ''; if (count($array) > 0) { foreach ($array as $k => $v) { $values .= sprintf( '%s %s => %s' . "\n", $whitespace, self::recursiveExport($k, $indentation), self::recursiveExport($value[$k], $indentation + 1, $processed) ); } $values = "\n" . $values . $whitespace; } return sprintf('Array &%s (%s)', $key, $values); } if (is_object($value)) { $class = get_class($value); if ($value instanceof ProphecyInterface) { return sprintf('%s Object (*Prophecy*)', $class); } elseif ($hash = $processed->contains($value)) { return sprintf('%s:%s Object', $class, $hash); } $hash = $processed->add($value); $values = ''; $array = self::toArray($value); if (count($array) > 0) { foreach ($array as $k => $v) { $values .= sprintf( '%s %s => %s' . "\n", $whitespace, self::recursiveExport($k, $indentation), self::recursiveExport($v, $indentation + 1, $processed) ); } $values = "\n" . $values . $whitespace; } return sprintf('%s:%s Object (%s)', $class, $hash, $values); } return var_export($value, true); } } * Marcello Duarte * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Prophecy\Util; use Prophecy\Call\Call; /** * String utility. * * @author Konstantin Kudryashov */ class StringUtil { /** * Stringifies any provided value. * * @param mixed $value * @param boolean $exportObject * * @return string */ public function stringify($value, $exportObject = true) { if (is_array($value)) { if (range(0, count($value) - 1) === array_keys($value)) { return '['.implode(', ', array_map(array($this, __FUNCTION__), $value)).']'; } $stringify = array($this, __FUNCTION__); return '['.implode(', ', array_map(function ($item, $key) use ($stringify) { return (is_integer($key) ? $key : '"'.$key.'"'). ' => '.call_user_func($stringify, $item); }, $value, array_keys($value))).']'; } if (is_resource($value)) { return get_resource_type($value).':'.$value; } if (is_object($value)) { return $exportObject ? ExportUtil::export($value) : sprintf('%s:%s', get_class($value), spl_object_hash($value)); } if (true === $value || false === $value) { return $value ? 'true' : 'false'; } if (is_string($value)) { $str = sprintf('"%s"', str_replace("\n", '\\n', $value)); if (50 <= strlen($str)) { return substr($str, 0, 50).'"...'; } return $str; } if (null === $value) { return 'null'; } return (string) $value; } /** * Stringifies provided array of calls. * * @param Call[] $calls Array of Call instances * * @return string */ public function stringifyCalls(array $calls) { $self = $this; return implode(PHP_EOL, array_map(function (Call $call) use ($self) { return sprintf(' - %s(%s) @ %s', $call->getMethodName(), implode(', ', array_map(array($self, 'stringify'), $call->getArguments())), str_replace(GETCWD().DIRECTORY_SEPARATOR, '', $call->getCallPlace()) ); }, $calls)); } } stop(); $writer = new PHP_CodeCoverage_Report_HTML; $writer->process($coverage, '/tmp/coverage'); filter(); $filter->addFileToBlacklist(__FILE__); $filter->addFileToBlacklist(dirname(__FILE__) . '/auto_append.php'); $coverage->start($_SERVER['SCRIPT_FILENAME']); * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Driver for HHVM's code coverage functionality. * * @since Class available since Release 2.2.2 * @codeCoverageIgnore */ class PHP_CodeCoverage_Driver_HHVM extends PHP_CodeCoverage_Driver_Xdebug { /** * Start collection of code coverage information. */ public function start() { xdebug_start_code_coverage(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Driver for PHPDBG's code coverage functionality. * * @since Class available since Release 2.2.0 * @codeCoverageIgnore */ class PHP_CodeCoverage_Driver_PHPDBG implements PHP_CodeCoverage_Driver { /** * Constructor. */ public function __construct() { if (PHP_SAPI !== 'phpdbg') { throw new PHP_CodeCoverage_Exception( 'This driver requires the PHPDBG SAPI' ); } if (!function_exists('phpdbg_start_oplog')) { throw new PHP_CodeCoverage_Exception( 'This build of PHPDBG does not support code coverage' ); } } /** * Start collection of code coverage information. */ public function start() { phpdbg_start_oplog(); } /** * Stop collection of code coverage information. * * @return array */ public function stop() { static $fetchedLines = array(); $dbgData = phpdbg_end_oplog(); if ($fetchedLines == array()) { $sourceLines = phpdbg_get_executable(); } else { $newFiles = array_diff( get_included_files(), array_keys($fetchedLines) ); if ($newFiles) { $sourceLines = phpdbg_get_executable( array('files' => $newFiles) ); } else { $sourceLines = array(); } } foreach ($sourceLines as $file => $lines) { foreach ($lines as $lineNo => $numExecuted) { $sourceLines[$file][$lineNo] = self::LINE_NOT_EXECUTED; } } $fetchedLines = array_merge($fetchedLines, $sourceLines); return $this->detectExecutedLines($fetchedLines, $dbgData); } /** * Convert phpdbg based data into the format CodeCoverage expects * * @param array $sourceLines * @param array $dbgData * @return array */ private function detectExecutedLines(array $sourceLines, array $dbgData) { foreach ($dbgData as $file => $coveredLines) { foreach ($coveredLines as $lineNo => $numExecuted) { // phpdbg also reports $lineNo=0 when e.g. exceptions get thrown. // make sure we only mark lines executed which are actually executable. if (isset($sourceLines[$file][$lineNo])) { $sourceLines[$file][$lineNo] = self::LINE_EXECUTED; } } } return $sourceLines; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Driver for Xdebug's code coverage functionality. * * @since Class available since Release 1.0.0 * @codeCoverageIgnore */ class PHP_CodeCoverage_Driver_Xdebug implements PHP_CodeCoverage_Driver { /** * Constructor. */ public function __construct() { if (!extension_loaded('xdebug')) { throw new PHP_CodeCoverage_Exception('This driver requires Xdebug'); } if (version_compare(phpversion('xdebug'), '2.2.0-dev', '>=') && !ini_get('xdebug.coverage_enable')) { throw new PHP_CodeCoverage_Exception( 'xdebug.coverage_enable=On has to be set in php.ini' ); } } /** * Start collection of code coverage information. */ public function start() { xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE); } /** * Stop collection of code coverage information. * * @return array */ public function stop() { $data = xdebug_get_code_coverage(); xdebug_stop_code_coverage(); return $this->cleanup($data); } /** * @param array $data * @return array * @since Method available since Release 2.0.0 */ private function cleanup(array $data) { foreach (array_keys($data) as $file) { unset($data[$file][0]); if ($file != 'xdebug://debug-eval' && file_exists($file)) { $numLines = $this->getNumberOfLinesInFile($file); foreach (array_keys($data[$file]) as $line) { if (isset($data[$file][$line]) && $line > $numLines) { unset($data[$file][$line]); } } } } return $data; } /** * @param string $file * @return int * @since Method available since Release 2.0.0 */ private function getNumberOfLinesInFile($file) { $buffer = file_get_contents($file); $lines = substr_count($buffer, "\n"); if (substr($buffer, -1) !== "\n") { $lines++; } return $lines; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Interface for code coverage drivers. * * @since Class available since Release 1.0.0 */ interface PHP_CodeCoverage_Driver { /** * @var int * @see http://xdebug.org/docs/code_coverage */ const LINE_EXECUTED = 1; /** * @var int * @see http://xdebug.org/docs/code_coverage */ const LINE_NOT_EXECUTED = -1; /** * @var int * @see http://xdebug.org/docs/code_coverage */ const LINE_NOT_EXECUTABLE = -2; /** * Start collection of code coverage information. */ public function start(); /** * Stop collection of code coverage information. * * @return array */ public function stop(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Exception that is raised when code is unintentionally covered. * * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Exception_UnintentionallyCoveredCode extends PHP_CodeCoverage_Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Exception class for PHP_CodeCoverage component. * * @since Class available since Release 1.1.0 */ class PHP_CodeCoverage_Exception extends RuntimeException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Filter for blacklisting and whitelisting of code coverage information. * * @since Class available since Release 1.0.0 */ class PHP_CodeCoverage_Filter { /** * Source files that are blacklisted. * * @var array */ private $blacklistedFiles = array(); /** * Source files that are whitelisted. * * @var array */ private $whitelistedFiles = array(); /** * Adds a directory to the blacklist (recursively). * * @param string $directory * @param string $suffix * @param string $prefix */ public function addDirectoryToBlacklist($directory, $suffix = '.php', $prefix = '') { $facade = new File_Iterator_Facade; $files = $facade->getFilesAsArray($directory, $suffix, $prefix); foreach ($files as $file) { $this->addFileToBlacklist($file); } } /** * Adds a file to the blacklist. * * @param string $filename */ public function addFileToBlacklist($filename) { $this->blacklistedFiles[realpath($filename)] = true; } /** * Adds files to the blacklist. * * @param array $files */ public function addFilesToBlacklist(array $files) { foreach ($files as $file) { $this->addFileToBlacklist($file); } } /** * Removes a directory from the blacklist (recursively). * * @param string $directory * @param string $suffix * @param string $prefix */ public function removeDirectoryFromBlacklist($directory, $suffix = '.php', $prefix = '') { $facade = new File_Iterator_Facade; $files = $facade->getFilesAsArray($directory, $suffix, $prefix); foreach ($files as $file) { $this->removeFileFromBlacklist($file); } } /** * Removes a file from the blacklist. * * @param string $filename */ public function removeFileFromBlacklist($filename) { $filename = realpath($filename); if (isset($this->blacklistedFiles[$filename])) { unset($this->blacklistedFiles[$filename]); } } /** * Adds a directory to the whitelist (recursively). * * @param string $directory * @param string $suffix * @param string $prefix */ public function addDirectoryToWhitelist($directory, $suffix = '.php', $prefix = '') { $facade = new File_Iterator_Facade; $files = $facade->getFilesAsArray($directory, $suffix, $prefix); foreach ($files as $file) { $this->addFileToWhitelist($file); } } /** * Adds a file to the whitelist. * * @param string $filename */ public function addFileToWhitelist($filename) { $this->whitelistedFiles[realpath($filename)] = true; } /** * Adds files to the whitelist. * * @param array $files */ public function addFilesToWhitelist(array $files) { foreach ($files as $file) { $this->addFileToWhitelist($file); } } /** * Removes a directory from the whitelist (recursively). * * @param string $directory * @param string $suffix * @param string $prefix */ public function removeDirectoryFromWhitelist($directory, $suffix = '.php', $prefix = '') { $facade = new File_Iterator_Facade; $files = $facade->getFilesAsArray($directory, $suffix, $prefix); foreach ($files as $file) { $this->removeFileFromWhitelist($file); } } /** * Removes a file from the whitelist. * * @param string $filename */ public function removeFileFromWhitelist($filename) { $filename = realpath($filename); if (isset($this->whitelistedFiles[$filename])) { unset($this->whitelistedFiles[$filename]); } } /** * Checks whether a filename is a real filename. * * @param string $filename * @return bool */ public function isFile($filename) { if ($filename == '-' || strpos($filename, 'vfs://') === 0 || strpos($filename, 'xdebug://debug-eval') !== false || strpos($filename, 'eval()\'d code') !== false || strpos($filename, 'runtime-created function') !== false || strpos($filename, 'runkit created function') !== false || strpos($filename, 'assert code') !== false || strpos($filename, 'regexp code') !== false) { return false; } return file_exists($filename); } /** * Checks whether or not a file is filtered. * * When the whitelist is empty (default), blacklisting is used. * When the whitelist is not empty, whitelisting is used. * * @param string $filename * @return bool * @throws PHP_CodeCoverage_Exception */ public function isFiltered($filename) { if (!$this->isFile($filename)) { return true; } $filename = realpath($filename); if (!empty($this->whitelistedFiles)) { return !isset($this->whitelistedFiles[$filename]); } return isset($this->blacklistedFiles[$filename]); } /** * Returns the list of blacklisted files. * * @return array */ public function getBlacklist() { return array_keys($this->blacklistedFiles); } /** * Returns the list of whitelisted files. * * @return array */ public function getWhitelist() { return array_keys($this->whitelistedFiles); } /** * Returns whether this filter has a whitelist. * * @return bool * @since Method available since Release 1.1.0 */ public function hasWhitelist() { return !empty($this->whitelistedFiles); } /** * Returns the blacklisted files. * * @return array * @since Method available since Release 2.0.0 */ public function getBlacklistedFiles() { return $this->blacklistedFiles; } /** * Sets the blacklisted files. * * @param array $blacklistedFiles * @since Method available since Release 2.0.0 */ public function setBlacklistedFiles($blacklistedFiles) { $this->blacklistedFiles = $blacklistedFiles; } /** * Returns the whitelisted files. * * @return array * @since Method available since Release 2.0.0 */ public function getWhitelistedFiles() { return $this->whitelistedFiles; } /** * Sets the whitelisted files. * * @param array $whitelistedFiles * @since Method available since Release 2.0.0 */ public function setWhitelistedFiles($whitelistedFiles) { $this->whitelistedFiles = $whitelistedFiles; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Generates a Clover XML logfile from an PHP_CodeCoverage object. * * @since Class available since Release 1.0.0 */ class PHP_CodeCoverage_Report_Clover { /** * @param PHP_CodeCoverage $coverage * @param string $target * @param string $name * @return string */ public function process(PHP_CodeCoverage $coverage, $target = null, $name = null) { $xmlDocument = new DOMDocument('1.0', 'UTF-8'); $xmlDocument->formatOutput = true; $xmlCoverage = $xmlDocument->createElement('coverage'); $xmlCoverage->setAttribute('generated', (int) $_SERVER['REQUEST_TIME']); $xmlDocument->appendChild($xmlCoverage); $xmlProject = $xmlDocument->createElement('project'); $xmlProject->setAttribute('timestamp', (int) $_SERVER['REQUEST_TIME']); if (is_string($name)) { $xmlProject->setAttribute('name', $name); } $xmlCoverage->appendChild($xmlProject); $packages = array(); $report = $coverage->getReport(); unset($coverage); foreach ($report as $item) { $namespace = 'global'; if (!$item instanceof PHP_CodeCoverage_Report_Node_File) { continue; } $xmlFile = $xmlDocument->createElement('file'); $xmlFile->setAttribute('name', $item->getPath()); $classes = $item->getClassesAndTraits(); $coverage = $item->getCoverageData(); $lines = array(); foreach ($classes as $className => $class) { $classStatements = 0; $coveredClassStatements = 0; $coveredMethods = 0; $classMethods = 0; foreach ($class['methods'] as $methodName => $method) { if ($method['executableLines'] == 0) { continue; } $classMethods++; $classStatements += $method['executableLines']; $coveredClassStatements += $method['executedLines']; if ($method['coverage'] == 100) { $coveredMethods++; } $methodCount = 0; for ($i = $method['startLine']; $i <= $method['endLine']; $i++) { if (isset($coverage[$i]) && ($coverage[$i] !== null)) { $methodCount = max($methodCount, count($coverage[$i])); } } $lines[$method['startLine']] = array( 'count' => $methodCount, 'crap' => $method['crap'], 'type' => 'method', 'name' => $methodName ); } if (!empty($class['package']['namespace'])) { $namespace = $class['package']['namespace']; } $xmlClass = $xmlDocument->createElement('class'); $xmlClass->setAttribute('name', $className); $xmlClass->setAttribute('namespace', $namespace); if (!empty($class['package']['fullPackage'])) { $xmlClass->setAttribute( 'fullPackage', $class['package']['fullPackage'] ); } if (!empty($class['package']['category'])) { $xmlClass->setAttribute( 'category', $class['package']['category'] ); } if (!empty($class['package']['package'])) { $xmlClass->setAttribute( 'package', $class['package']['package'] ); } if (!empty($class['package']['subpackage'])) { $xmlClass->setAttribute( 'subpackage', $class['package']['subpackage'] ); } $xmlFile->appendChild($xmlClass); $xmlMetrics = $xmlDocument->createElement('metrics'); $xmlMetrics->setAttribute('methods', $classMethods); $xmlMetrics->setAttribute('coveredmethods', $coveredMethods); $xmlMetrics->setAttribute('conditionals', 0); $xmlMetrics->setAttribute('coveredconditionals', 0); $xmlMetrics->setAttribute('statements', $classStatements); $xmlMetrics->setAttribute( 'coveredstatements', $coveredClassStatements ); $xmlMetrics->setAttribute( 'elements', $classMethods + $classStatements /* + conditionals */ ); $xmlMetrics->setAttribute( 'coveredelements', $coveredMethods + $coveredClassStatements /* + coveredconditionals */ ); $xmlClass->appendChild($xmlMetrics); } foreach ($coverage as $line => $data) { if ($data === null || isset($lines[$line])) { continue; } $lines[$line] = array( 'count' => count($data), 'type' => 'stmt' ); } ksort($lines); foreach ($lines as $line => $data) { $xmlLine = $xmlDocument->createElement('line'); $xmlLine->setAttribute('num', $line); $xmlLine->setAttribute('type', $data['type']); if (isset($data['name'])) { $xmlLine->setAttribute('name', $data['name']); } if (isset($data['crap'])) { $xmlLine->setAttribute('crap', $data['crap']); } $xmlLine->setAttribute('count', $data['count']); $xmlFile->appendChild($xmlLine); } $linesOfCode = $item->getLinesOfCode(); $xmlMetrics = $xmlDocument->createElement('metrics'); $xmlMetrics->setAttribute('loc', $linesOfCode['loc']); $xmlMetrics->setAttribute('ncloc', $linesOfCode['ncloc']); $xmlMetrics->setAttribute('classes', $item->getNumClassesAndTraits()); $xmlMetrics->setAttribute('methods', $item->getNumMethods()); $xmlMetrics->setAttribute( 'coveredmethods', $item->getNumTestedMethods() ); $xmlMetrics->setAttribute('conditionals', 0); $xmlMetrics->setAttribute('coveredconditionals', 0); $xmlMetrics->setAttribute( 'statements', $item->getNumExecutableLines() ); $xmlMetrics->setAttribute( 'coveredstatements', $item->getNumExecutedLines() ); $xmlMetrics->setAttribute( 'elements', $item->getNumMethods() + $item->getNumExecutableLines() /* + conditionals */ ); $xmlMetrics->setAttribute( 'coveredelements', $item->getNumTestedMethods() + $item->getNumExecutedLines() /* + coveredconditionals */ ); $xmlFile->appendChild($xmlMetrics); if ($namespace == 'global') { $xmlProject->appendChild($xmlFile); } else { if (!isset($packages[$namespace])) { $packages[$namespace] = $xmlDocument->createElement( 'package' ); $packages[$namespace]->setAttribute('name', $namespace); $xmlProject->appendChild($packages[$namespace]); } $packages[$namespace]->appendChild($xmlFile); } } $linesOfCode = $report->getLinesOfCode(); $xmlMetrics = $xmlDocument->createElement('metrics'); $xmlMetrics->setAttribute('files', count($report)); $xmlMetrics->setAttribute('loc', $linesOfCode['loc']); $xmlMetrics->setAttribute('ncloc', $linesOfCode['ncloc']); $xmlMetrics->setAttribute( 'classes', $report->getNumClassesAndTraits() ); $xmlMetrics->setAttribute('methods', $report->getNumMethods()); $xmlMetrics->setAttribute( 'coveredmethods', $report->getNumTestedMethods() ); $xmlMetrics->setAttribute('conditionals', 0); $xmlMetrics->setAttribute('coveredconditionals', 0); $xmlMetrics->setAttribute( 'statements', $report->getNumExecutableLines() ); $xmlMetrics->setAttribute( 'coveredstatements', $report->getNumExecutedLines() ); $xmlMetrics->setAttribute( 'elements', $report->getNumMethods() + $report->getNumExecutableLines() /* + conditionals */ ); $xmlMetrics->setAttribute( 'coveredelements', $report->getNumTestedMethods() + $report->getNumExecutedLines() /* + coveredconditionals */ ); $xmlProject->appendChild($xmlMetrics); if ($target !== null) { if (!is_dir(dirname($target))) { mkdir(dirname($target), 0777, true); } return $xmlDocument->save($target); } else { return $xmlDocument->saveXML(); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_Crap4j { /** * @var int */ private $threshold; /** * @param int $threshold */ public function __construct($threshold = 30) { if (!is_int($threshold)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'integer' ); } $this->threshold = $threshold; } /** * @param PHP_CodeCoverage $coverage * @param string $target * @param string $name * @return string */ public function process(PHP_CodeCoverage $coverage, $target = null, $name = null) { $document = new DOMDocument('1.0', 'UTF-8'); $document->formatOutput = true; $root = $document->createElement('crap_result'); $document->appendChild($root); $project = $document->createElement('project', is_string($name) ? $name : ''); $root->appendChild($project); $root->appendChild($document->createElement('timestamp', date('Y-m-d H:i:s', (int) $_SERVER['REQUEST_TIME']))); $stats = $document->createElement('stats'); $methodsNode = $document->createElement('methods'); $report = $coverage->getReport(); unset($coverage); $fullMethodCount = 0; $fullCrapMethodCount = 0; $fullCrapLoad = 0; $fullCrap = 0; foreach ($report as $item) { $namespace = 'global'; if (!$item instanceof PHP_CodeCoverage_Report_Node_File) { continue; } $file = $document->createElement('file'); $file->setAttribute('name', $item->getPath()); $classes = $item->getClassesAndTraits(); foreach ($classes as $className => $class) { foreach ($class['methods'] as $methodName => $method) { $crapLoad = $this->getCrapLoad($method['crap'], $method['ccn'], $method['coverage']); $fullCrap += $method['crap']; $fullCrapLoad += $crapLoad; $fullMethodCount++; if ($method['crap'] >= $this->threshold) { $fullCrapMethodCount++; } $methodNode = $document->createElement('method'); if (!empty($class['package']['namespace'])) { $namespace = $class['package']['namespace']; } $methodNode->appendChild($document->createElement('package', $namespace)); $methodNode->appendChild($document->createElement('className', $className)); $methodNode->appendChild($document->createElement('methodName', $methodName)); $methodNode->appendChild($document->createElement('methodSignature', htmlspecialchars($method['signature']))); $methodNode->appendChild($document->createElement('fullMethod', htmlspecialchars($method['signature']))); $methodNode->appendChild($document->createElement('crap', $this->roundValue($method['crap']))); $methodNode->appendChild($document->createElement('complexity', $method['ccn'])); $methodNode->appendChild($document->createElement('coverage', $this->roundValue($method['coverage']))); $methodNode->appendChild($document->createElement('crapLoad', round($crapLoad))); $methodsNode->appendChild($methodNode); } } } $stats->appendChild($document->createElement('name', 'Method Crap Stats')); $stats->appendChild($document->createElement('methodCount', $fullMethodCount)); $stats->appendChild($document->createElement('crapMethodCount', $fullCrapMethodCount)); $stats->appendChild($document->createElement('crapLoad', round($fullCrapLoad))); $stats->appendChild($document->createElement('totalCrap', $fullCrap)); if ($fullMethodCount > 0) { $crapMethodPercent = $this->roundValue((100 * $fullCrapMethodCount) / $fullMethodCount); } else { $crapMethodPercent = 0; } $stats->appendChild($document->createElement('crapMethodPercent', $crapMethodPercent)); $root->appendChild($stats); $root->appendChild($methodsNode); if ($target !== null) { if (!is_dir(dirname($target))) { mkdir(dirname($target), 0777, true); } return $document->save($target); } else { return $document->saveXML(); } } /** * @param float $crapValue * @param int $cyclomaticComplexity * @param float $coveragePercent * @return float */ private function getCrapLoad($crapValue, $cyclomaticComplexity, $coveragePercent) { $crapLoad = 0; if ($crapValue >= $this->threshold) { $crapLoad += $cyclomaticComplexity * (1.0 - $coveragePercent / 100); $crapLoad += $cyclomaticComplexity / $this->threshold; } return $crapLoad; } /** * @param float $value * @return float */ private function roundValue($value) { return round($value, 2); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Factory for PHP_CodeCoverage_Report_Node_* object graphs. * * @since Class available since Release 1.1.0 */ class PHP_CodeCoverage_Report_Factory { /** * @param PHP_CodeCoverage $coverage * @return PHP_CodeCoverage_Report_Node_Directory */ public function create(PHP_CodeCoverage $coverage) { $files = $coverage->getData(); $commonPath = $this->reducePaths($files); $root = new PHP_CodeCoverage_Report_Node_Directory( $commonPath, null ); $this->addItems( $root, $this->buildDirectoryStructure($files), $coverage->getTests(), $coverage->getCacheTokens() ); return $root; } /** * @param PHP_CodeCoverage_Report_Node_Directory $root * @param array $items * @param array $tests * @param bool $cacheTokens */ private function addItems(PHP_CodeCoverage_Report_Node_Directory $root, array $items, array $tests, $cacheTokens) { foreach ($items as $key => $value) { if (substr($key, -2) == '/f') { $key = substr($key, 0, -2); if (file_exists($root->getPath() . DIRECTORY_SEPARATOR . $key)) { $root->addFile($key, $value, $tests, $cacheTokens); } } else { $child = $root->addDirectory($key); $this->addItems($child, $value, $tests, $cacheTokens); } } } /** * Builds an array representation of the directory structure. * * For instance, * * * Array * ( * [Money.php] => Array * ( * ... * ) * * [MoneyBag.php] => Array * ( * ... * ) * ) * * * is transformed into * * * Array * ( * [.] => Array * ( * [Money.php] => Array * ( * ... * ) * * [MoneyBag.php] => Array * ( * ... * ) * ) * ) * * * @param array $files * @return array */ private function buildDirectoryStructure($files) { $result = array(); foreach ($files as $path => $file) { $path = explode('/', $path); $pointer = &$result; $max = count($path); for ($i = 0; $i < $max; $i++) { if ($i == ($max - 1)) { $type = '/f'; } else { $type = ''; } $pointer = &$pointer[$path[$i] . $type]; } $pointer = $file; } return $result; } /** * Reduces the paths by cutting the longest common start path. * * For instance, * * * Array * ( * [/home/sb/Money/Money.php] => Array * ( * ... * ) * * [/home/sb/Money/MoneyBag.php] => Array * ( * ... * ) * ) * * * is reduced to * * * Array * ( * [Money.php] => Array * ( * ... * ) * * [MoneyBag.php] => Array * ( * ... * ) * ) * * * @param array $files * @return string */ private function reducePaths(&$files) { if (empty($files)) { return '.'; } $commonPath = ''; $paths = array_keys($files); if (count($files) == 1) { $commonPath = dirname($paths[0]) . '/'; $files[basename($paths[0])] = $files[$paths[0]]; unset($files[$paths[0]]); return $commonPath; } $max = count($paths); for ($i = 0; $i < $max; $i++) { // strip phar:// prefixes if (strpos($paths[$i], 'phar://') === 0) { $paths[$i] = substr($paths[$i], 7); $paths[$i] = strtr($paths[$i], '/', DIRECTORY_SEPARATOR); } $paths[$i] = explode(DIRECTORY_SEPARATOR, $paths[$i]); if (empty($paths[$i][0])) { $paths[$i][0] = DIRECTORY_SEPARATOR; } } $done = false; $max = count($paths); while (!$done) { for ($i = 0; $i < $max - 1; $i++) { if (!isset($paths[$i][0]) || !isset($paths[$i+1][0]) || $paths[$i][0] != $paths[$i+1][0]) { $done = true; break; } } if (!$done) { $commonPath .= $paths[0][0]; if ($paths[0][0] != DIRECTORY_SEPARATOR) { $commonPath .= DIRECTORY_SEPARATOR; } for ($i = 0; $i < $max; $i++) { array_shift($paths[$i]); } } } $original = array_keys($files); $max = count($original); for ($i = 0; $i < $max; $i++) { $files[implode('/', $paths[$i])] = $files[$original[$i]]; unset($files[$original[$i]]); } ksort($files); return substr($commonPath, 0, -1); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Renders the dashboard for a PHP_CodeCoverage_Report_Node_Directory node. * * @since Class available since Release 1.1.0 */ class PHP_CodeCoverage_Report_HTML_Renderer_Dashboard extends PHP_CodeCoverage_Report_HTML_Renderer { /** * @param PHP_CodeCoverage_Report_Node_Directory $node * @param string $file */ public function render(PHP_CodeCoverage_Report_Node_Directory $node, $file) { $classes = $node->getClassesAndTraits(); $template = new Text_Template( $this->templatePath . 'dashboard.html', '{{', '}}' ); $this->setCommonTemplateVariables($template, $node); $baseLink = $node->getId() . '/'; $complexity = $this->complexity($classes, $baseLink); $coverageDistribution = $this->coverageDistribution($classes); $insufficientCoverage = $this->insufficientCoverage($classes, $baseLink); $projectRisks = $this->projectRisks($classes, $baseLink); $template->setVar( array( 'insufficient_coverage_classes' => $insufficientCoverage['class'], 'insufficient_coverage_methods' => $insufficientCoverage['method'], 'project_risks_classes' => $projectRisks['class'], 'project_risks_methods' => $projectRisks['method'], 'complexity_class' => $complexity['class'], 'complexity_method' => $complexity['method'], 'class_coverage_distribution' => $coverageDistribution['class'], 'method_coverage_distribution' => $coverageDistribution['method'] ) ); $template->renderTo($file); } /** * Returns the data for the Class/Method Complexity charts. * * @param array $classes * @param string $baseLink * @return array */ protected function complexity(array $classes, $baseLink) { $result = array('class' => array(), 'method' => array()); foreach ($classes as $className => $class) { foreach ($class['methods'] as $methodName => $method) { if ($className != '*') { $methodName = $className . '::' . $methodName; } $result['method'][] = array( $method['coverage'], $method['ccn'], sprintf( '%s', str_replace($baseLink, '', $method['link']), $methodName ) ); } $result['class'][] = array( $class['coverage'], $class['ccn'], sprintf( '%s', str_replace($baseLink, '', $class['link']), $className ) ); } return array( 'class' => json_encode($result['class']), 'method' => json_encode($result['method']) ); } /** * Returns the data for the Class / Method Coverage Distribution chart. * * @param array $classes * @return array */ protected function coverageDistribution(array $classes) { $result = array( 'class' => array( '0%' => 0, '0-10%' => 0, '10-20%' => 0, '20-30%' => 0, '30-40%' => 0, '40-50%' => 0, '50-60%' => 0, '60-70%' => 0, '70-80%' => 0, '80-90%' => 0, '90-100%' => 0, '100%' => 0 ), 'method' => array( '0%' => 0, '0-10%' => 0, '10-20%' => 0, '20-30%' => 0, '30-40%' => 0, '40-50%' => 0, '50-60%' => 0, '60-70%' => 0, '70-80%' => 0, '80-90%' => 0, '90-100%' => 0, '100%' => 0 ) ); foreach ($classes as $class) { foreach ($class['methods'] as $methodName => $method) { if ($method['coverage'] == 0) { $result['method']['0%']++; } elseif ($method['coverage'] == 100) { $result['method']['100%']++; } else { $key = floor($method['coverage'] / 10) * 10; $key = $key . '-' . ($key + 10) . '%'; $result['method'][$key]++; } } if ($class['coverage'] == 0) { $result['class']['0%']++; } elseif ($class['coverage'] == 100) { $result['class']['100%']++; } else { $key = floor($class['coverage'] / 10) * 10; $key = $key . '-' . ($key + 10) . '%'; $result['class'][$key]++; } } return array( 'class' => json_encode(array_values($result['class'])), 'method' => json_encode(array_values($result['method'])) ); } /** * Returns the classes / methods with insufficient coverage. * * @param array $classes * @param string $baseLink * @return array */ protected function insufficientCoverage(array $classes, $baseLink) { $leastTestedClasses = array(); $leastTestedMethods = array(); $result = array('class' => '', 'method' => ''); foreach ($classes as $className => $class) { foreach ($class['methods'] as $methodName => $method) { if ($method['coverage'] < $this->highLowerBound) { if ($className != '*') { $key = $className . '::' . $methodName; } else { $key = $methodName; } $leastTestedMethods[$key] = $method['coverage']; } } if ($class['coverage'] < $this->highLowerBound) { $leastTestedClasses[$className] = $class['coverage']; } } asort($leastTestedClasses); asort($leastTestedMethods); foreach ($leastTestedClasses as $className => $coverage) { $result['class'] .= sprintf( ' %s%d%%' . "\n", str_replace($baseLink, '', $classes[$className]['link']), $className, $coverage ); } foreach ($leastTestedMethods as $methodName => $coverage) { list($class, $method) = explode('::', $methodName); $result['method'] .= sprintf( ' %s%d%%' . "\n", str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']), $methodName, $method, $coverage ); } return $result; } /** * Returns the project risks according to the CRAP index. * * @param array $classes * @param string $baseLink * @return array */ protected function projectRisks(array $classes, $baseLink) { $classRisks = array(); $methodRisks = array(); $result = array('class' => '', 'method' => ''); foreach ($classes as $className => $class) { foreach ($class['methods'] as $methodName => $method) { if ($method['coverage'] < $this->highLowerBound && $method['ccn'] > 1) { if ($className != '*') { $key = $className . '::' . $methodName; } else { $key = $methodName; } $methodRisks[$key] = $method['crap']; } } if ($class['coverage'] < $this->highLowerBound && $class['ccn'] > count($class['methods'])) { $classRisks[$className] = $class['crap']; } } arsort($classRisks); arsort($methodRisks); foreach ($classRisks as $className => $crap) { $result['class'] .= sprintf( ' %s%d' . "\n", str_replace($baseLink, '', $classes[$className]['link']), $className, $crap ); } foreach ($methodRisks as $methodName => $crap) { list($class, $method) = explode('::', $methodName); $result['method'] .= sprintf( ' %s%d' . "\n", str_replace($baseLink, '', $classes[$class]['methods'][$method]['link']), $methodName, $method, $crap ); } return $result; } protected function getActiveBreadcrumb(PHP_CodeCoverage_Report_Node $node) { return sprintf( '
  • %s
  • ' . "\n" . '
  • (Dashboard)
  • ' . "\n", $node->getName() ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Renders a PHP_CodeCoverage_Report_Node_Directory node. * * @since Class available since Release 1.1.0 */ class PHP_CodeCoverage_Report_HTML_Renderer_Directory extends PHP_CodeCoverage_Report_HTML_Renderer { /** * @param PHP_CodeCoverage_Report_Node_Directory $node * @param string $file */ public function render(PHP_CodeCoverage_Report_Node_Directory $node, $file) { $template = new Text_Template($this->templatePath . 'directory.html', '{{', '}}'); $this->setCommonTemplateVariables($template, $node); $items = $this->renderItem($node, true); foreach ($node->getDirectories() as $item) { $items .= $this->renderItem($item); } foreach ($node->getFiles() as $item) { $items .= $this->renderItem($item); } $template->setVar( array( 'id' => $node->getId(), 'items' => $items ) ); $template->renderTo($file); } /** * @param PHP_CodeCoverage_Report_Node $item * @param bool $total * @return string */ protected function renderItem(PHP_CodeCoverage_Report_Node $item, $total = false) { $data = array( 'numClasses' => $item->getNumClassesAndTraits(), 'numTestedClasses' => $item->getNumTestedClassesAndTraits(), 'numMethods' => $item->getNumMethods(), 'numTestedMethods' => $item->getNumTestedMethods(), 'linesExecutedPercent' => $item->getLineExecutedPercent(false), 'linesExecutedPercentAsString' => $item->getLineExecutedPercent(), 'numExecutedLines' => $item->getNumExecutedLines(), 'numExecutableLines' => $item->getNumExecutableLines(), 'testedMethodsPercent' => $item->getTestedMethodsPercent(false), 'testedMethodsPercentAsString' => $item->getTestedMethodsPercent(), 'testedClassesPercent' => $item->getTestedClassesAndTraitsPercent(false), 'testedClassesPercentAsString' => $item->getTestedClassesAndTraitsPercent() ); if ($total) { $data['name'] = 'Total'; } else { if ($item instanceof PHP_CodeCoverage_Report_Node_Directory) { $data['name'] = sprintf( '%s', $item->getName(), $item->getName() ); $data['icon'] = ' '; } else { $data['name'] = sprintf( '%s', $item->getName(), $item->getName() ); $data['icon'] = ' '; } } return $this->renderItemTemplate( new Text_Template($this->templatePath . 'directory_item.html', '{{', '}}'), $data ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ // @codeCoverageIgnoreStart if (!defined('T_TRAIT')) { define('T_TRAIT', 1001); } if (!defined('T_INSTEADOF')) { define('T_INSTEADOF', 1002); } if (!defined('T_CALLABLE')) { define('T_CALLABLE', 1003); } if (!defined('T_FINALLY')) { define('T_FINALLY', 1004); } if (!defined('T_YIELD')) { define('T_YIELD', 1005); } // @codeCoverageIgnoreEnd /** * Renders a PHP_CodeCoverage_Report_Node_File node. * * @since Class available since Release 1.1.0 */ class PHP_CodeCoverage_Report_HTML_Renderer_File extends PHP_CodeCoverage_Report_HTML_Renderer { /** * @var int */ private $htmlspecialcharsFlags; /** * Constructor. * * @param string $templatePath * @param string $generator * @param string $date * @param int $lowUpperBound * @param int $highLowerBound */ public function __construct($templatePath, $generator, $date, $lowUpperBound, $highLowerBound) { parent::__construct( $templatePath, $generator, $date, $lowUpperBound, $highLowerBound ); $this->htmlspecialcharsFlags = ENT_COMPAT; if (PHP_VERSION_ID >= 50400 && defined('ENT_SUBSTITUTE')) { $this->htmlspecialcharsFlags = $this->htmlspecialcharsFlags | ENT_HTML401 | ENT_SUBSTITUTE; } } /** * @param PHP_CodeCoverage_Report_Node_File $node * @param string $file */ public function render(PHP_CodeCoverage_Report_Node_File $node, $file) { $template = new Text_Template($this->templatePath . 'file.html', '{{', '}}'); $template->setVar( array( 'items' => $this->renderItems($node), 'lines' => $this->renderSource($node) ) ); $this->setCommonTemplateVariables($template, $node); $template->renderTo($file); } /** * @param PHP_CodeCoverage_Report_Node_File $node * @return string */ protected function renderItems(PHP_CodeCoverage_Report_Node_File $node) { $template = new Text_Template($this->templatePath . 'file_item.html', '{{', '}}'); $methodItemTemplate = new Text_Template( $this->templatePath . 'method_item.html', '{{', '}}' ); $items = $this->renderItemTemplate( $template, array( 'name' => 'Total', 'numClasses' => $node->getNumClassesAndTraits(), 'numTestedClasses' => $node->getNumTestedClassesAndTraits(), 'numMethods' => $node->getNumMethods(), 'numTestedMethods' => $node->getNumTestedMethods(), 'linesExecutedPercent' => $node->getLineExecutedPercent(false), 'linesExecutedPercentAsString' => $node->getLineExecutedPercent(), 'numExecutedLines' => $node->getNumExecutedLines(), 'numExecutableLines' => $node->getNumExecutableLines(), 'testedMethodsPercent' => $node->getTestedMethodsPercent(false), 'testedMethodsPercentAsString' => $node->getTestedMethodsPercent(), 'testedClassesPercent' => $node->getTestedClassesAndTraitsPercent(false), 'testedClassesPercentAsString' => $node->getTestedClassesAndTraitsPercent(), 'crap' => 'CRAP' ) ); $items .= $this->renderFunctionItems( $node->getFunctions(), $methodItemTemplate ); $items .= $this->renderTraitOrClassItems( $node->getTraits(), $template, $methodItemTemplate ); $items .= $this->renderTraitOrClassItems( $node->getClasses(), $template, $methodItemTemplate ); return $items; } /** * @param array $items * @param Text_Template $template * @param Text_Template $methodItemTemplate * @return string */ protected function renderTraitOrClassItems(array $items, Text_Template $template, Text_Template $methodItemTemplate) { if (empty($items)) { return ''; } $buffer = ''; foreach ($items as $name => $item) { $numMethods = count($item['methods']); $numTestedMethods = 0; foreach ($item['methods'] as $method) { if ($method['executedLines'] == $method['executableLines']) { $numTestedMethods++; } } $buffer .= $this->renderItemTemplate( $template, array( 'name' => $name, 'numClasses' => 1, 'numTestedClasses' => $numTestedMethods == $numMethods ? 1 : 0, 'numMethods' => $numMethods, 'numTestedMethods' => $numTestedMethods, 'linesExecutedPercent' => PHP_CodeCoverage_Util::percent( $item['executedLines'], $item['executableLines'], false ), 'linesExecutedPercentAsString' => PHP_CodeCoverage_Util::percent( $item['executedLines'], $item['executableLines'], true ), 'numExecutedLines' => $item['executedLines'], 'numExecutableLines' => $item['executableLines'], 'testedMethodsPercent' => PHP_CodeCoverage_Util::percent( $numTestedMethods, $numMethods, false ), 'testedMethodsPercentAsString' => PHP_CodeCoverage_Util::percent( $numTestedMethods, $numMethods, true ), 'testedClassesPercent' => PHP_CodeCoverage_Util::percent( $numTestedMethods == $numMethods ? 1 : 0, 1, false ), 'testedClassesPercentAsString' => PHP_CodeCoverage_Util::percent( $numTestedMethods == $numMethods ? 1 : 0, 1, true ), 'crap' => $item['crap'] ) ); foreach ($item['methods'] as $method) { $buffer .= $this->renderFunctionOrMethodItem( $methodItemTemplate, $method, ' ' ); } } return $buffer; } /** * @param array $functions * @param Text_Template $template * @return string */ protected function renderFunctionItems(array $functions, Text_Template $template) { if (empty($functions)) { return ''; } $buffer = ''; foreach ($functions as $function) { $buffer .= $this->renderFunctionOrMethodItem( $template, $function ); } return $buffer; } /** * @param Text_Template $template * @return string */ protected function renderFunctionOrMethodItem(Text_Template $template, array $item, $indent = '') { $numTestedItems = $item['executedLines'] == $item['executableLines'] ? 1 : 0; return $this->renderItemTemplate( $template, array( 'name' => sprintf( '%s%s', $indent, $item['startLine'], htmlspecialchars($item['signature']), isset($item['functionName']) ? $item['functionName'] : $item['methodName'] ), 'numMethods' => 1, 'numTestedMethods' => $numTestedItems, 'linesExecutedPercent' => PHP_CodeCoverage_Util::percent( $item['executedLines'], $item['executableLines'], false ), 'linesExecutedPercentAsString' => PHP_CodeCoverage_Util::percent( $item['executedLines'], $item['executableLines'], true ), 'numExecutedLines' => $item['executedLines'], 'numExecutableLines' => $item['executableLines'], 'testedMethodsPercent' => PHP_CodeCoverage_Util::percent( $numTestedItems, 1, false ), 'testedMethodsPercentAsString' => PHP_CodeCoverage_Util::percent( $numTestedItems, 1, true ), 'crap' => $item['crap'] ) ); } /** * @param PHP_CodeCoverage_Report_Node_File $node * @return string */ protected function renderSource(PHP_CodeCoverage_Report_Node_File $node) { $coverageData = $node->getCoverageData(); $testData = $node->getTestData(); $codeLines = $this->loadFile($node->getPath()); $lines = ''; $i = 1; foreach ($codeLines as $line) { $trClass = ''; $popoverContent = ''; $popoverTitle = ''; if (array_key_exists($i, $coverageData)) { $numTests = count($coverageData[$i]); if ($coverageData[$i] === null) { $trClass = ' class="warning"'; } elseif ($numTests == 0) { $trClass = ' class="danger"'; } else { $lineCss = 'covered-by-large-tests'; $popoverContent = '
      '; if ($numTests > 1) { $popoverTitle = $numTests . ' tests cover line ' . $i; } else { $popoverTitle = '1 test covers line ' . $i; } foreach ($coverageData[$i] as $test) { if ($lineCss == 'covered-by-large-tests' && $testData[$test]['size'] == 'medium') { $lineCss = 'covered-by-medium-tests'; } elseif ($testData[$test]['size'] == 'small') { $lineCss = 'covered-by-small-tests'; } switch ($testData[$test]['status']) { case 0: switch ($testData[$test]['size']) { case 'small': $testCSS = ' class="covered-by-small-tests"'; break; case 'medium': $testCSS = ' class="covered-by-medium-tests"'; break; default: $testCSS = ' class="covered-by-large-tests"'; break; } break; case 1: case 2: $testCSS = ' class="warning"'; break; case 3: $testCSS = ' class="danger"'; break; case 4: $testCSS = ' class="danger"'; break; default: $testCSS = ''; } $popoverContent .= sprintf( '%s', $testCSS, htmlspecialchars($test) ); } $popoverContent .= '
    '; $trClass = ' class="' . $lineCss . ' popin"'; } } if (!empty($popoverTitle)) { $popover = sprintf( ' data-title="%s" data-content="%s" data-placement="bottom" data-html="true"', $popoverTitle, htmlspecialchars($popoverContent) ); } else { $popover = ''; } $lines .= sprintf( ' %s' . "\n", $trClass, $popover, $i, $i, $i, $line ); $i++; } return $lines; } /** * @param string $file * @return array */ protected function loadFile($file) { $buffer = file_get_contents($file); $tokens = token_get_all($buffer); $result = array(''); $i = 0; $stringFlag = false; $fileEndsWithNewLine = substr($buffer, -1) == "\n"; unset($buffer); foreach ($tokens as $j => $token) { if (is_string($token)) { if ($token === '"' && $tokens[$j - 1] !== '\\') { $result[$i] .= sprintf( '%s', htmlspecialchars($token) ); $stringFlag = !$stringFlag; } else { $result[$i] .= sprintf( '%s', htmlspecialchars($token) ); } continue; } list($token, $value) = $token; $value = str_replace( array("\t", ' '), array('    ', ' '), htmlspecialchars($value, $this->htmlspecialcharsFlags) ); if ($value === "\n") { $result[++$i] = ''; } else { $lines = explode("\n", $value); foreach ($lines as $jj => $line) { $line = trim($line); if ($line !== '') { if ($stringFlag) { $colour = 'string'; } else { switch ($token) { case T_INLINE_HTML: $colour = 'html'; break; case T_COMMENT: case T_DOC_COMMENT: $colour = 'comment'; break; case T_ABSTRACT: case T_ARRAY: case T_AS: case T_BREAK: case T_CALLABLE: case T_CASE: case T_CATCH: case T_CLASS: case T_CLONE: case T_CONTINUE: case T_DEFAULT: case T_ECHO: case T_ELSE: case T_ELSEIF: case T_EMPTY: case T_ENDDECLARE: case T_ENDFOR: case T_ENDFOREACH: case T_ENDIF: case T_ENDSWITCH: case T_ENDWHILE: case T_EXIT: case T_EXTENDS: case T_FINAL: case T_FINALLY: case T_FOREACH: case T_FUNCTION: case T_GLOBAL: case T_IF: case T_IMPLEMENTS: case T_INCLUDE: case T_INCLUDE_ONCE: case T_INSTANCEOF: case T_INSTEADOF: case T_INTERFACE: case T_ISSET: case T_LOGICAL_AND: case T_LOGICAL_OR: case T_LOGICAL_XOR: case T_NAMESPACE: case T_NEW: case T_PRIVATE: case T_PROTECTED: case T_PUBLIC: case T_REQUIRE: case T_REQUIRE_ONCE: case T_RETURN: case T_STATIC: case T_THROW: case T_TRAIT: case T_TRY: case T_UNSET: case T_USE: case T_VAR: case T_WHILE: case T_YIELD: $colour = 'keyword'; break; default: $colour = 'default'; } } $result[$i] .= sprintf( '%s', $colour, $line ); } if (isset($lines[$jj + 1])) { $result[++$i] = ''; } } } } if ($fileEndsWithNewLine) { unset($result[count($result)-1]); } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Environment\Runtime; /** * Base class for PHP_CodeCoverage_Report_Node renderers. * * @since Class available since Release 1.1.0 */ abstract class PHP_CodeCoverage_Report_HTML_Renderer { /** * @var string */ protected $templatePath; /** * @var string */ protected $generator; /** * @var string */ protected $date; /** * @var int */ protected $lowUpperBound; /** * @var int */ protected $highLowerBound; /** * @var string */ protected $version; /** * Constructor. * * @param string $templatePath * @param string $generator * @param string $date * @param int $lowUpperBound * @param int $highLowerBound */ public function __construct($templatePath, $generator, $date, $lowUpperBound, $highLowerBound) { $version = new SebastianBergmann\Version('2.2.4', dirname(dirname(dirname(dirname(__DIR__))))); $this->templatePath = $templatePath; $this->generator = $generator; $this->date = $date; $this->lowUpperBound = $lowUpperBound; $this->highLowerBound = $highLowerBound; $this->version = $version->getVersion(); } /** * @param Text_Template $template * @param array $data * @return string */ protected function renderItemTemplate(Text_Template $template, array $data) { $numSeparator = ' / '; if (isset($data['numClasses']) && $data['numClasses'] > 0) { $classesLevel = $this->getColorLevel($data['testedClassesPercent']); $classesNumber = $data['numTestedClasses'] . $numSeparator . $data['numClasses']; $classesBar = $this->getCoverageBar( $data['testedClassesPercent'] ); } else { $classesLevel = 'success'; $classesNumber = '0' . $numSeparator . '0'; $classesBar = $this->getCoverageBar(100); } if ($data['numMethods'] > 0) { $methodsLevel = $this->getColorLevel($data['testedMethodsPercent']); $methodsNumber = $data['numTestedMethods'] . $numSeparator . $data['numMethods']; $methodsBar = $this->getCoverageBar( $data['testedMethodsPercent'] ); } else { $methodsLevel = 'success'; $methodsNumber = '0' . $numSeparator . '0'; $methodsBar = $this->getCoverageBar(100); $data['testedMethodsPercentAsString'] = '100.00%'; } if ($data['numExecutableLines'] > 0) { $linesLevel = $this->getColorLevel($data['linesExecutedPercent']); $linesNumber = $data['numExecutedLines'] . $numSeparator . $data['numExecutableLines']; $linesBar = $this->getCoverageBar( $data['linesExecutedPercent'] ); } else { $linesLevel = 'success'; $linesNumber = '0' . $numSeparator . '0'; $linesBar = $this->getCoverageBar(100); $data['linesExecutedPercentAsString'] = '100.00%'; } $template->setVar( array( 'icon' => isset($data['icon']) ? $data['icon'] : '', 'crap' => isset($data['crap']) ? $data['crap'] : '', 'name' => $data['name'], 'lines_bar' => $linesBar, 'lines_executed_percent' => $data['linesExecutedPercentAsString'], 'lines_level' => $linesLevel, 'lines_number' => $linesNumber, 'methods_bar' => $methodsBar, 'methods_tested_percent' => $data['testedMethodsPercentAsString'], 'methods_level' => $methodsLevel, 'methods_number' => $methodsNumber, 'classes_bar' => $classesBar, 'classes_tested_percent' => isset($data['testedClassesPercentAsString']) ? $data['testedClassesPercentAsString'] : '', 'classes_level' => $classesLevel, 'classes_number' => $classesNumber ) ); return $template->render(); } /** * @param Text_Template $template * @param PHP_CodeCoverage_Report_Node $node */ protected function setCommonTemplateVariables(Text_Template $template, PHP_CodeCoverage_Report_Node $node) { $runtime = new Runtime; $template->setVar( array( 'id' => $node->getId(), 'full_path' => $node->getPath(), 'path_to_root' => $this->getPathToRoot($node), 'breadcrumbs' => $this->getBreadcrumbs($node), 'date' => $this->date, 'version' => $this->version, 'runtime_name' => $runtime->getName(), 'runtime_version' => $runtime->getVersion(), 'runtime_link' => $runtime->getVendorUrl(), 'generator' => $this->generator, 'low_upper_bound' => $this->lowUpperBound, 'high_lower_bound' => $this->highLowerBound ) ); } protected function getBreadcrumbs(PHP_CodeCoverage_Report_Node $node) { $breadcrumbs = ''; $path = $node->getPathAsArray(); $pathToRoot = array(); $max = count($path); if ($node instanceof PHP_CodeCoverage_Report_Node_File) { $max--; } for ($i = 0; $i < $max; $i++) { $pathToRoot[] = str_repeat('../', $i); } foreach ($path as $step) { if ($step !== $node) { $breadcrumbs .= $this->getInactiveBreadcrumb( $step, array_pop($pathToRoot) ); } else { $breadcrumbs .= $this->getActiveBreadcrumb($step); } } return $breadcrumbs; } protected function getActiveBreadcrumb(PHP_CodeCoverage_Report_Node $node) { $buffer = sprintf( '
  • %s
  • ' . "\n", $node->getName() ); if ($node instanceof PHP_CodeCoverage_Report_Node_Directory) { $buffer .= '
  • (Dashboard)
  • ' . "\n"; } return $buffer; } protected function getInactiveBreadcrumb(PHP_CodeCoverage_Report_Node $node, $pathToRoot) { return sprintf( '
  • %s
  • ' . "\n", $pathToRoot, $node->getName() ); } protected function getPathToRoot(PHP_CodeCoverage_Report_Node $node) { $id = $node->getId(); $depth = substr_count($id, '/'); if ($id != 'index' && $node instanceof PHP_CodeCoverage_Report_Node_Directory) { $depth++; } return str_repeat('../', $depth); } protected function getCoverageBar($percent) { $level = $this->getColorLevel($percent); $template = new Text_Template( $this->templatePath . 'coverage_bar.html', '{{', '}}' ); $template->setVar(array('level' => $level, 'percent' => sprintf('%.2F', $percent))); return $template->render(); } /** * @param int $percent * @return string */ protected function getColorLevel($percent) { if ($percent <= $this->lowUpperBound) { return 'danger'; } elseif ($percent > $this->lowUpperBound && $percent < $this->highLowerBound) { return 'warning'; } else { return 'success'; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Generates an HTML report from an PHP_CodeCoverage object. * * @since Class available since Release 1.0.0 */ class PHP_CodeCoverage_Report_HTML { /** * @var string */ private $templatePath; /** * @var string */ private $generator; /** * @var int */ private $lowUpperBound; /** * @var int */ private $highLowerBound; /** * Constructor. * * @param int $lowUpperBound * @param int $highLowerBound * @param string $generator */ public function __construct($lowUpperBound = 50, $highLowerBound = 90, $generator = '') { $this->generator = $generator; $this->highLowerBound = $highLowerBound; $this->lowUpperBound = $lowUpperBound; $this->templatePath = sprintf( '%s%sHTML%sRenderer%sTemplate%s', dirname(__FILE__), DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR ); } /** * @param PHP_CodeCoverage $coverage * @param string $target */ public function process(PHP_CodeCoverage $coverage, $target) { $target = $this->getDirectory($target); $report = $coverage->getReport(); unset($coverage); if (!isset($_SERVER['REQUEST_TIME'])) { $_SERVER['REQUEST_TIME'] = time(); } $date = date('D M j G:i:s T Y', $_SERVER['REQUEST_TIME']); $dashboard = new PHP_CodeCoverage_Report_HTML_Renderer_Dashboard( $this->templatePath, $this->generator, $date, $this->lowUpperBound, $this->highLowerBound ); $directory = new PHP_CodeCoverage_Report_HTML_Renderer_Directory( $this->templatePath, $this->generator, $date, $this->lowUpperBound, $this->highLowerBound ); $file = new PHP_CodeCoverage_Report_HTML_Renderer_File( $this->templatePath, $this->generator, $date, $this->lowUpperBound, $this->highLowerBound ); $directory->render($report, $target . 'index.html'); $dashboard->render($report, $target . 'dashboard.html'); foreach ($report as $node) { $id = $node->getId(); if ($node instanceof PHP_CodeCoverage_Report_Node_Directory) { if (!file_exists($target . $id)) { mkdir($target . $id, 0777, true); } $directory->render($node, $target . $id . '/index.html'); $dashboard->render($node, $target . $id . '/dashboard.html'); } else { $dir = dirname($target . $id); if (!file_exists($dir)) { mkdir($dir, 0777, true); } $file->render($node, $target . $id . '.html'); } } $this->copyFiles($target); } /** * @param string $target */ private function copyFiles($target) { $dir = $this->getDirectory($target . 'css'); copy($this->templatePath . 'css/bootstrap.min.css', $dir . 'bootstrap.min.css'); copy($this->templatePath . 'css/nv.d3.min.css', $dir . 'nv.d3.min.css'); copy($this->templatePath . 'css/style.css', $dir . 'style.css'); $dir = $this->getDirectory($target . 'fonts'); copy($this->templatePath . 'fonts/glyphicons-halflings-regular.eot', $dir . 'glyphicons-halflings-regular.eot'); copy($this->templatePath . 'fonts/glyphicons-halflings-regular.svg', $dir . 'glyphicons-halflings-regular.svg'); copy($this->templatePath . 'fonts/glyphicons-halflings-regular.ttf', $dir . 'glyphicons-halflings-regular.ttf'); copy($this->templatePath . 'fonts/glyphicons-halflings-regular.woff', $dir . 'glyphicons-halflings-regular.woff'); copy($this->templatePath . 'fonts/glyphicons-halflings-regular.woff2', $dir . 'glyphicons-halflings-regular.woff2'); $dir = $this->getDirectory($target . 'js'); copy($this->templatePath . 'js/bootstrap.min.js', $dir . 'bootstrap.min.js'); copy($this->templatePath . 'js/d3.min.js', $dir . 'd3.min.js'); copy($this->templatePath . 'js/holder.min.js', $dir . 'holder.min.js'); copy($this->templatePath . 'js/html5shiv.min.js', $dir . 'html5shiv.min.js'); copy($this->templatePath . 'js/jquery.min.js', $dir . 'jquery.min.js'); copy($this->templatePath . 'js/nv.d3.min.js', $dir . 'nv.d3.min.js'); copy($this->templatePath . 'js/respond.min.js', $dir . 'respond.min.js'); } /** * @param string $directory * @return string * @throws PHP_CodeCoverage_Exception * @since Method available since Release 1.2.0 */ private function getDirectory($directory) { if (substr($directory, -1, 1) != DIRECTORY_SEPARATOR) { $directory .= DIRECTORY_SEPARATOR; } if (is_dir($directory)) { return $directory; } if (@mkdir($directory, 0777, true)) { return $directory; } throw new PHP_CodeCoverage_Exception( sprintf( 'Directory "%s" does not exist.', $directory ) ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Represents a directory in the code coverage information tree. * * @since Class available since Release 1.1.0 */ class PHP_CodeCoverage_Report_Node_Directory extends PHP_CodeCoverage_Report_Node implements IteratorAggregate { /** * @var PHP_CodeCoverage_Report_Node[] */ protected $children = array(); /** * @var PHP_CodeCoverage_Report_Node_Directory[] */ protected $directories = array(); /** * @var PHP_CodeCoverage_Report_Node_File[] */ protected $files = array(); /** * @var array */ protected $classes; /** * @var array */ protected $traits; /** * @var array */ protected $functions; /** * @var array */ protected $linesOfCode = null; /** * @var int */ protected $numFiles = -1; /** * @var int */ protected $numExecutableLines = -1; /** * @var int */ protected $numExecutedLines = -1; /** * @var int */ protected $numClasses = -1; /** * @var int */ protected $numTestedClasses = -1; /** * @var int */ protected $numTraits = -1; /** * @var int */ protected $numTestedTraits = -1; /** * @var int */ protected $numMethods = -1; /** * @var int */ protected $numTestedMethods = -1; /** * @var int */ protected $numFunctions = -1; /** * @var int */ protected $numTestedFunctions = -1; /** * Returns the number of files in/under this node. * * @return int */ public function count() { if ($this->numFiles == -1) { $this->numFiles = 0; foreach ($this->children as $child) { $this->numFiles += count($child); } } return $this->numFiles; } /** * Returns an iterator for this node. * * @return RecursiveIteratorIterator */ public function getIterator() { return new RecursiveIteratorIterator( new PHP_CodeCoverage_Report_Node_Iterator($this), RecursiveIteratorIterator::SELF_FIRST ); } /** * Adds a new directory. * * @param string $name * @return PHP_CodeCoverage_Report_Node_Directory */ public function addDirectory($name) { $directory = new self($name, $this); $this->children[] = $directory; $this->directories[] = &$this->children[count($this->children) - 1]; return $directory; } /** * Adds a new file. * * @param string $name * @param array $coverageData * @param array $testData * @param bool $cacheTokens * @return PHP_CodeCoverage_Report_Node_File * @throws PHP_CodeCoverage_Exception */ public function addFile($name, array $coverageData, array $testData, $cacheTokens) { $file = new PHP_CodeCoverage_Report_Node_File( $name, $this, $coverageData, $testData, $cacheTokens ); $this->children[] = $file; $this->files[] = &$this->children[count($this->children) - 1]; $this->numExecutableLines = -1; $this->numExecutedLines = -1; return $file; } /** * Returns the directories in this directory. * * @return array */ public function getDirectories() { return $this->directories; } /** * Returns the files in this directory. * * @return array */ public function getFiles() { return $this->files; } /** * Returns the child nodes of this node. * * @return array */ public function getChildNodes() { return $this->children; } /** * Returns the classes of this node. * * @return array */ public function getClasses() { if ($this->classes === null) { $this->classes = array(); foreach ($this->children as $child) { $this->classes = array_merge( $this->classes, $child->getClasses() ); } } return $this->classes; } /** * Returns the traits of this node. * * @return array */ public function getTraits() { if ($this->traits === null) { $this->traits = array(); foreach ($this->children as $child) { $this->traits = array_merge( $this->traits, $child->getTraits() ); } } return $this->traits; } /** * Returns the functions of this node. * * @return array */ public function getFunctions() { if ($this->functions === null) { $this->functions = array(); foreach ($this->children as $child) { $this->functions = array_merge( $this->functions, $child->getFunctions() ); } } return $this->functions; } /** * Returns the LOC/CLOC/NCLOC of this node. * * @return array */ public function getLinesOfCode() { if ($this->linesOfCode === null) { $this->linesOfCode = array('loc' => 0, 'cloc' => 0, 'ncloc' => 0); foreach ($this->children as $child) { $linesOfCode = $child->getLinesOfCode(); $this->linesOfCode['loc'] += $linesOfCode['loc']; $this->linesOfCode['cloc'] += $linesOfCode['cloc']; $this->linesOfCode['ncloc'] += $linesOfCode['ncloc']; } } return $this->linesOfCode; } /** * Returns the number of executable lines. * * @return int */ public function getNumExecutableLines() { if ($this->numExecutableLines == -1) { $this->numExecutableLines = 0; foreach ($this->children as $child) { $this->numExecutableLines += $child->getNumExecutableLines(); } } return $this->numExecutableLines; } /** * Returns the number of executed lines. * * @return int */ public function getNumExecutedLines() { if ($this->numExecutedLines == -1) { $this->numExecutedLines = 0; foreach ($this->children as $child) { $this->numExecutedLines += $child->getNumExecutedLines(); } } return $this->numExecutedLines; } /** * Returns the number of classes. * * @return int */ public function getNumClasses() { if ($this->numClasses == -1) { $this->numClasses = 0; foreach ($this->children as $child) { $this->numClasses += $child->getNumClasses(); } } return $this->numClasses; } /** * Returns the number of tested classes. * * @return int */ public function getNumTestedClasses() { if ($this->numTestedClasses == -1) { $this->numTestedClasses = 0; foreach ($this->children as $child) { $this->numTestedClasses += $child->getNumTestedClasses(); } } return $this->numTestedClasses; } /** * Returns the number of traits. * * @return int */ public function getNumTraits() { if ($this->numTraits == -1) { $this->numTraits = 0; foreach ($this->children as $child) { $this->numTraits += $child->getNumTraits(); } } return $this->numTraits; } /** * Returns the number of tested traits. * * @return int */ public function getNumTestedTraits() { if ($this->numTestedTraits == -1) { $this->numTestedTraits = 0; foreach ($this->children as $child) { $this->numTestedTraits += $child->getNumTestedTraits(); } } return $this->numTestedTraits; } /** * Returns the number of methods. * * @return int */ public function getNumMethods() { if ($this->numMethods == -1) { $this->numMethods = 0; foreach ($this->children as $child) { $this->numMethods += $child->getNumMethods(); } } return $this->numMethods; } /** * Returns the number of tested methods. * * @return int */ public function getNumTestedMethods() { if ($this->numTestedMethods == -1) { $this->numTestedMethods = 0; foreach ($this->children as $child) { $this->numTestedMethods += $child->getNumTestedMethods(); } } return $this->numTestedMethods; } /** * Returns the number of functions. * * @return int */ public function getNumFunctions() { if ($this->numFunctions == -1) { $this->numFunctions = 0; foreach ($this->children as $child) { $this->numFunctions += $child->getNumFunctions(); } } return $this->numFunctions; } /** * Returns the number of tested functions. * * @return int */ public function getNumTestedFunctions() { if ($this->numTestedFunctions == -1) { $this->numTestedFunctions = 0; foreach ($this->children as $child) { $this->numTestedFunctions += $child->getNumTestedFunctions(); } } return $this->numTestedFunctions; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Represents a file in the code coverage information tree. * * @since Class available since Release 1.1.0 */ class PHP_CodeCoverage_Report_Node_File extends PHP_CodeCoverage_Report_Node { /** * @var array */ protected $coverageData; /** * @var array */ protected $testData; /** * @var int */ protected $numExecutableLines = 0; /** * @var int */ protected $numExecutedLines = 0; /** * @var array */ protected $classes = array(); /** * @var array */ protected $traits = array(); /** * @var array */ protected $functions = array(); /** * @var array */ protected $linesOfCode = array(); /** * @var int */ protected $numTestedTraits = 0; /** * @var int */ protected $numTestedClasses = 0; /** * @var int */ protected $numMethods = null; /** * @var int */ protected $numTestedMethods = null; /** * @var int */ protected $numTestedFunctions = null; /** * @var array */ protected $startLines = array(); /** * @var array */ protected $endLines = array(); /** * @var bool */ protected $cacheTokens; /** * Constructor. * * @param string $name * @param PHP_CodeCoverage_Report_Node $parent * @param array $coverageData * @param array $testData * @param bool $cacheTokens * @throws PHP_CodeCoverage_Exception */ public function __construct($name, PHP_CodeCoverage_Report_Node $parent, array $coverageData, array $testData, $cacheTokens) { if (!is_bool($cacheTokens)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'boolean' ); } parent::__construct($name, $parent); $this->coverageData = $coverageData; $this->testData = $testData; $this->cacheTokens = $cacheTokens; $this->calculateStatistics(); } /** * Returns the number of files in/under this node. * * @return int */ public function count() { return 1; } /** * Returns the code coverage data of this node. * * @return array */ public function getCoverageData() { return $this->coverageData; } /** * Returns the test data of this node. * * @return array */ public function getTestData() { return $this->testData; } /** * Returns the classes of this node. * * @return array */ public function getClasses() { return $this->classes; } /** * Returns the traits of this node. * * @return array */ public function getTraits() { return $this->traits; } /** * Returns the functions of this node. * * @return array */ public function getFunctions() { return $this->functions; } /** * Returns the LOC/CLOC/NCLOC of this node. * * @return array */ public function getLinesOfCode() { return $this->linesOfCode; } /** * Returns the number of executable lines. * * @return int */ public function getNumExecutableLines() { return $this->numExecutableLines; } /** * Returns the number of executed lines. * * @return int */ public function getNumExecutedLines() { return $this->numExecutedLines; } /** * Returns the number of classes. * * @return int */ public function getNumClasses() { return count($this->classes); } /** * Returns the number of tested classes. * * @return int */ public function getNumTestedClasses() { return $this->numTestedClasses; } /** * Returns the number of traits. * * @return int */ public function getNumTraits() { return count($this->traits); } /** * Returns the number of tested traits. * * @return int */ public function getNumTestedTraits() { return $this->numTestedTraits; } /** * Returns the number of methods. * * @return int */ public function getNumMethods() { if ($this->numMethods === null) { $this->numMethods = 0; foreach ($this->classes as $class) { foreach ($class['methods'] as $method) { if ($method['executableLines'] > 0) { $this->numMethods++; } } } foreach ($this->traits as $trait) { foreach ($trait['methods'] as $method) { if ($method['executableLines'] > 0) { $this->numMethods++; } } } } return $this->numMethods; } /** * Returns the number of tested methods. * * @return int */ public function getNumTestedMethods() { if ($this->numTestedMethods === null) { $this->numTestedMethods = 0; foreach ($this->classes as $class) { foreach ($class['methods'] as $method) { if ($method['executableLines'] > 0 && $method['coverage'] == 100) { $this->numTestedMethods++; } } } foreach ($this->traits as $trait) { foreach ($trait['methods'] as $method) { if ($method['executableLines'] > 0 && $method['coverage'] == 100) { $this->numTestedMethods++; } } } } return $this->numTestedMethods; } /** * Returns the number of functions. * * @return int */ public function getNumFunctions() { return count($this->functions); } /** * Returns the number of tested functions. * * @return int */ public function getNumTestedFunctions() { if ($this->numTestedFunctions === null) { $this->numTestedFunctions = 0; foreach ($this->functions as $function) { if ($function['executableLines'] > 0 && $function['coverage'] == 100) { $this->numTestedFunctions++; } } } return $this->numTestedFunctions; } /** * Calculates coverage statistics for the file. */ protected function calculateStatistics() { $classStack = $functionStack = array(); if ($this->cacheTokens) { $tokens = PHP_Token_Stream_CachingFactory::get($this->getPath()); } else { $tokens = new PHP_Token_Stream($this->getPath()); } $this->processClasses($tokens); $this->processTraits($tokens); $this->processFunctions($tokens); $this->linesOfCode = $tokens->getLinesOfCode(); unset($tokens); for ($lineNumber = 1; $lineNumber <= $this->linesOfCode['loc']; $lineNumber++) { if (isset($this->startLines[$lineNumber])) { // Start line of a class. if (isset($this->startLines[$lineNumber]['className'])) { if (isset($currentClass)) { $classStack[] = &$currentClass; } $currentClass = &$this->startLines[$lineNumber]; } // Start line of a trait. elseif (isset($this->startLines[$lineNumber]['traitName'])) { $currentTrait = &$this->startLines[$lineNumber]; } // Start line of a method. elseif (isset($this->startLines[$lineNumber]['methodName'])) { $currentMethod = &$this->startLines[$lineNumber]; } // Start line of a function. elseif (isset($this->startLines[$lineNumber]['functionName'])) { if (isset($currentFunction)) { $functionStack[] = &$currentFunction; } $currentFunction = &$this->startLines[$lineNumber]; } } if (isset($this->coverageData[$lineNumber])) { if (isset($currentClass)) { $currentClass['executableLines']++; } if (isset($currentTrait)) { $currentTrait['executableLines']++; } if (isset($currentMethod)) { $currentMethod['executableLines']++; } if (isset($currentFunction)) { $currentFunction['executableLines']++; } $this->numExecutableLines++; if (count($this->coverageData[$lineNumber]) > 0) { if (isset($currentClass)) { $currentClass['executedLines']++; } if (isset($currentTrait)) { $currentTrait['executedLines']++; } if (isset($currentMethod)) { $currentMethod['executedLines']++; } if (isset($currentFunction)) { $currentFunction['executedLines']++; } $this->numExecutedLines++; } } if (isset($this->endLines[$lineNumber])) { // End line of a class. if (isset($this->endLines[$lineNumber]['className'])) { unset($currentClass); if ($classStack) { end($classStack); $key = key($classStack); $currentClass = &$classStack[$key]; unset($classStack[$key]); } } // End line of a trait. elseif (isset($this->endLines[$lineNumber]['traitName'])) { unset($currentTrait); } // End line of a method. elseif (isset($this->endLines[$lineNumber]['methodName'])) { unset($currentMethod); } // End line of a function. elseif (isset($this->endLines[$lineNumber]['functionName'])) { unset($currentFunction); if ($functionStack) { end($functionStack); $key = key($functionStack); $currentFunction = &$functionStack[$key]; unset($functionStack[$key]); } } } } foreach ($this->traits as &$trait) { foreach ($trait['methods'] as &$method) { if ($method['executableLines'] > 0) { $method['coverage'] = ($method['executedLines'] / $method['executableLines']) * 100; } else { $method['coverage'] = 100; } $method['crap'] = $this->crap( $method['ccn'], $method['coverage'] ); $trait['ccn'] += $method['ccn']; } if ($trait['executableLines'] > 0) { $trait['coverage'] = ($trait['executedLines'] / $trait['executableLines']) * 100; } else { $trait['coverage'] = 100; } if ($trait['coverage'] == 100) { $this->numTestedClasses++; } $trait['crap'] = $this->crap( $trait['ccn'], $trait['coverage'] ); } foreach ($this->classes as &$class) { foreach ($class['methods'] as &$method) { if ($method['executableLines'] > 0) { $method['coverage'] = ($method['executedLines'] / $method['executableLines']) * 100; } else { $method['coverage'] = 100; } $method['crap'] = $this->crap( $method['ccn'], $method['coverage'] ); $class['ccn'] += $method['ccn']; } if ($class['executableLines'] > 0) { $class['coverage'] = ($class['executedLines'] / $class['executableLines']) * 100; } else { $class['coverage'] = 100; } if ($class['coverage'] == 100) { $this->numTestedClasses++; } $class['crap'] = $this->crap( $class['ccn'], $class['coverage'] ); } } /** * @param PHP_Token_Stream $tokens */ protected function processClasses(PHP_Token_Stream $tokens) { $classes = $tokens->getClasses(); unset($tokens); $link = $this->getId() . '.html#'; foreach ($classes as $className => $class) { $this->classes[$className] = array( 'className' => $className, 'methods' => array(), 'startLine' => $class['startLine'], 'executableLines' => 0, 'executedLines' => 0, 'ccn' => 0, 'coverage' => 0, 'crap' => 0, 'package' => $class['package'], 'link' => $link . $class['startLine'] ); $this->startLines[$class['startLine']] = &$this->classes[$className]; $this->endLines[$class['endLine']] = &$this->classes[$className]; foreach ($class['methods'] as $methodName => $method) { $this->classes[$className]['methods'][$methodName] = array( 'methodName' => $methodName, 'signature' => $method['signature'], 'startLine' => $method['startLine'], 'endLine' => $method['endLine'], 'executableLines' => 0, 'executedLines' => 0, 'ccn' => $method['ccn'], 'coverage' => 0, 'crap' => 0, 'link' => $link . $method['startLine'] ); $this->startLines[$method['startLine']] = &$this->classes[$className]['methods'][$methodName]; $this->endLines[$method['endLine']] = &$this->classes[$className]['methods'][$methodName]; } } } /** * @param PHP_Token_Stream $tokens */ protected function processTraits(PHP_Token_Stream $tokens) { $traits = $tokens->getTraits(); unset($tokens); $link = $this->getId() . '.html#'; foreach ($traits as $traitName => $trait) { $this->traits[$traitName] = array( 'traitName' => $traitName, 'methods' => array(), 'startLine' => $trait['startLine'], 'executableLines' => 0, 'executedLines' => 0, 'ccn' => 0, 'coverage' => 0, 'crap' => 0, 'package' => $trait['package'], 'link' => $link . $trait['startLine'] ); $this->startLines[$trait['startLine']] = &$this->traits[$traitName]; $this->endLines[$trait['endLine']] = &$this->traits[$traitName]; foreach ($trait['methods'] as $methodName => $method) { $this->traits[$traitName]['methods'][$methodName] = array( 'methodName' => $methodName, 'signature' => $method['signature'], 'startLine' => $method['startLine'], 'endLine' => $method['endLine'], 'executableLines' => 0, 'executedLines' => 0, 'ccn' => $method['ccn'], 'coverage' => 0, 'crap' => 0, 'link' => $link . $method['startLine'] ); $this->startLines[$method['startLine']] = &$this->traits[$traitName]['methods'][$methodName]; $this->endLines[$method['endLine']] = &$this->traits[$traitName]['methods'][$methodName]; } } } /** * @param PHP_Token_Stream $tokens */ protected function processFunctions(PHP_Token_Stream $tokens) { $functions = $tokens->getFunctions(); unset($tokens); $link = $this->getId() . '.html#'; foreach ($functions as $functionName => $function) { $this->functions[$functionName] = array( 'functionName' => $functionName, 'signature' => $function['signature'], 'startLine' => $function['startLine'], 'executableLines' => 0, 'executedLines' => 0, 'ccn' => $function['ccn'], 'coverage' => 0, 'crap' => 0, 'link' => $link . $function['startLine'] ); $this->startLines[$function['startLine']] = &$this->functions[$functionName]; $this->endLines[$function['endLine']] = &$this->functions[$functionName]; } } /** * Calculates the Change Risk Anti-Patterns (CRAP) index for a unit of code * based on its cyclomatic complexity and percentage of code coverage. * * @param int $ccn * @param float $coverage * @return string * @since Method available since Release 1.2.0 */ protected function crap($ccn, $coverage) { if ($coverage == 0) { return (string) (pow($ccn, 2) + $ccn); } if ($coverage >= 95) { return (string) $ccn; } return sprintf( '%01.2F', pow($ccn, 2) * pow(1 - $coverage/100, 3) + $ccn ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Recursive iterator for PHP_CodeCoverage_Report_Node object graphs. * * @since Class available since Release 1.1.0 */ class PHP_CodeCoverage_Report_Node_Iterator implements RecursiveIterator { /** * @var int */ protected $position; /** * @var PHP_CodeCoverage_Report_Node[] */ protected $nodes; /** * Constructor. * * @param PHP_CodeCoverage_Report_Node_Directory $node */ public function __construct(PHP_CodeCoverage_Report_Node_Directory $node) { $this->nodes = $node->getChildNodes(); } /** * Rewinds the Iterator to the first element. */ public function rewind() { $this->position = 0; } /** * Checks if there is a current element after calls to rewind() or next(). * * @return bool */ public function valid() { return $this->position < count($this->nodes); } /** * Returns the key of the current element. * * @return int */ public function key() { return $this->position; } /** * Returns the current element. * * @return PHPUnit_Framework_Test */ public function current() { return $this->valid() ? $this->nodes[$this->position] : null; } /** * Moves forward to next element. */ public function next() { $this->position++; } /** * Returns the sub iterator for the current element. * * @return PHP_CodeCoverage_Report_Node_Iterator */ public function getChildren() { return new self( $this->nodes[$this->position] ); } /** * Checks whether the current element has children. * * @return bool */ public function hasChildren() { return $this->nodes[$this->position] instanceof PHP_CodeCoverage_Report_Node_Directory; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Base class for nodes in the code coverage information tree. * * @since Class available since Release 1.1.0 */ abstract class PHP_CodeCoverage_Report_Node implements Countable { /** * @var string */ protected $name; /** * @var string */ protected $path; /** * @var array */ protected $pathArray; /** * @var PHP_CodeCoverage_Report_Node */ protected $parent; /** * @var string */ protected $id; /** * Constructor. * * @param string $name * @param PHP_CodeCoverage_Report_Node $parent */ public function __construct($name, PHP_CodeCoverage_Report_Node $parent = null) { if (substr($name, -1) == '/') { $name = substr($name, 0, -1); } $this->name = $name; $this->parent = $parent; } /** * @return string */ public function getName() { return $this->name; } /** * @return string */ public function getId() { if ($this->id === null) { $parent = $this->getParent(); if ($parent === null) { $this->id = 'index'; } else { $parentId = $parent->getId(); if ($parentId == 'index') { $this->id = str_replace(':', '_', $this->name); } else { $this->id = $parentId . '/' . $this->name; } } } return $this->id; } /** * @return string */ public function getPath() { if ($this->path === null) { if ($this->parent === null || $this->parent->getPath() === null || $this->parent->getPath() === false) { $this->path = $this->name; } else { $this->path = $this->parent->getPath() . '/' . $this->name; } } return $this->path; } /** * @return array */ public function getPathAsArray() { if ($this->pathArray === null) { if ($this->parent === null) { $this->pathArray = array(); } else { $this->pathArray = $this->parent->getPathAsArray(); } $this->pathArray[] = $this; } return $this->pathArray; } /** * @return PHP_CodeCoverage_Report_Node */ public function getParent() { return $this->parent; } /** * Returns the percentage of classes that has been tested. * * @param bool $asString * @return int */ public function getTestedClassesPercent($asString = true) { return PHP_CodeCoverage_Util::percent( $this->getNumTestedClasses(), $this->getNumClasses(), $asString ); } /** * Returns the percentage of traits that has been tested. * * @param bool $asString * @return int */ public function getTestedTraitsPercent($asString = true) { return PHP_CodeCoverage_Util::percent( $this->getNumTestedTraits(), $this->getNumTraits(), $asString ); } /** * Returns the percentage of traits that has been tested. * * @param bool $asString * @return int * @since Method available since Release 1.2.0 */ public function getTestedClassesAndTraitsPercent($asString = true) { return PHP_CodeCoverage_Util::percent( $this->getNumTestedClassesAndTraits(), $this->getNumClassesAndTraits(), $asString ); } /** * Returns the percentage of methods that has been tested. * * @param bool $asString * @return int */ public function getTestedMethodsPercent($asString = true) { return PHP_CodeCoverage_Util::percent( $this->getNumTestedMethods(), $this->getNumMethods(), $asString ); } /** * Returns the percentage of executed lines. * * @param bool $asString * @return int */ public function getLineExecutedPercent($asString = true) { return PHP_CodeCoverage_Util::percent( $this->getNumExecutedLines(), $this->getNumExecutableLines(), $asString ); } /** * Returns the number of classes and traits. * * @return int * @since Method available since Release 1.2.0 */ public function getNumClassesAndTraits() { return $this->getNumClasses() + $this->getNumTraits(); } /** * Returns the number of tested classes and traits. * * @return int * @since Method available since Release 1.2.0 */ public function getNumTestedClassesAndTraits() { return $this->getNumTestedClasses() + $this->getNumTestedTraits(); } /** * Returns the classes and traits of this node. * * @return array * @since Method available since Release 1.2.0 */ public function getClassesAndTraits() { return array_merge($this->getClasses(), $this->getTraits()); } /** * Returns the classes of this node. * * @return array */ abstract public function getClasses(); /** * Returns the traits of this node. * * @return array */ abstract public function getTraits(); /** * Returns the functions of this node. * * @return array */ abstract public function getFunctions(); /** * Returns the LOC/CLOC/NCLOC of this node. * * @return array */ abstract public function getLinesOfCode(); /** * Returns the number of executable lines. * * @return int */ abstract public function getNumExecutableLines(); /** * Returns the number of executed lines. * * @return int */ abstract public function getNumExecutedLines(); /** * Returns the number of classes. * * @return int */ abstract public function getNumClasses(); /** * Returns the number of tested classes. * * @return int */ abstract public function getNumTestedClasses(); /** * Returns the number of traits. * * @return int */ abstract public function getNumTraits(); /** * Returns the number of tested traits. * * @return int */ abstract public function getNumTestedTraits(); /** * Returns the number of methods. * * @return int */ abstract public function getNumMethods(); /** * Returns the number of tested methods. * * @return int */ abstract public function getNumTestedMethods(); /** * Returns the number of functions. * * @return int */ abstract public function getNumFunctions(); /** * Returns the number of tested functions. * * @return int */ abstract public function getNumTestedFunctions(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Uses var_export() to write a PHP_CodeCoverage object to a file. * * @since Class available since Release 1.1.0 */ class PHP_CodeCoverage_Report_PHP { /** * @param PHP_CodeCoverage $coverage * @param string $target * @return string */ public function process(PHP_CodeCoverage $coverage, $target = null) { $filter = $coverage->filter(); $output = sprintf( 'setData(%s); $coverage->setTests(%s); $filter = $coverage->filter(); $filter->setBlacklistedFiles(%s); $filter->setWhitelistedFiles(%s); return $coverage;', var_export($coverage->getData(true), 1), var_export($coverage->getTests(), 1), var_export($filter->getBlacklistedFiles(), 1), var_export($filter->getWhitelistedFiles(), 1) ); if ($target !== null) { return file_put_contents($target, $output); } else { return $output; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Generates human readable output from an PHP_CodeCoverage object. * * The output gets put into a text file our written to the CLI. * * @since Class available since Release 1.1.0 */ class PHP_CodeCoverage_Report_Text { protected $lowUpperBound; protected $highLowerBound; protected $showUncoveredFiles; protected $showOnlySummary; protected $colors = array( 'green' => "\x1b[30;42m", 'yellow' => "\x1b[30;43m", 'red' => "\x1b[37;41m", 'header' => "\x1b[1;37;40m", 'reset' => "\x1b[0m", 'eol' => "\x1b[2K", ); public function __construct($lowUpperBound, $highLowerBound, $showUncoveredFiles, $showOnlySummary) { $this->lowUpperBound = $lowUpperBound; $this->highLowerBound = $highLowerBound; $this->showUncoveredFiles = $showUncoveredFiles; $this->showOnlySummary = $showOnlySummary; } /** * @param PHP_CodeCoverage $coverage * @param bool $showColors * @return string */ public function process(PHP_CodeCoverage $coverage, $showColors = false) { $output = PHP_EOL . PHP_EOL; $report = $coverage->getReport(); unset($coverage); $colors = array( 'header' => '', 'classes' => '', 'methods' => '', 'lines' => '', 'reset' => '', 'eol' => '' ); if ($showColors) { $colors['classes'] = $this->getCoverageColor( $report->getNumTestedClassesAndTraits(), $report->getNumClassesAndTraits() ); $colors['methods'] = $this->getCoverageColor( $report->getNumTestedMethods(), $report->getNumMethods() ); $colors['lines'] = $this->getCoverageColor( $report->getNumExecutedLines(), $report->getNumExecutableLines() ); $colors['reset'] = $this->colors['reset']; $colors['header'] = $this->colors['header']; $colors['eol'] = $this->colors['eol']; } $classes = sprintf( ' Classes: %6s (%d/%d)', PHP_CodeCoverage_Util::percent( $report->getNumTestedClassesAndTraits(), $report->getNumClassesAndTraits(), true ), $report->getNumTestedClassesAndTraits(), $report->getNumClassesAndTraits() ); $methods = sprintf( ' Methods: %6s (%d/%d)', PHP_CodeCoverage_Util::percent( $report->getNumTestedMethods(), $report->getNumMethods(), true ), $report->getNumTestedMethods(), $report->getNumMethods() ); $lines = sprintf( ' Lines: %6s (%d/%d)', PHP_CodeCoverage_Util::percent( $report->getNumExecutedLines(), $report->getNumExecutableLines(), true ), $report->getNumExecutedLines(), $report->getNumExecutableLines() ); $padding = max(array_map('strlen', array($classes, $methods, $lines))); if ($this->showOnlySummary) { $title = 'Code Coverage Report Summary:'; $padding = max($padding, strlen($title)); $output .= $this->format($colors['header'], $padding, $title); } else { $date = date(' Y-m-d H:i:s', $_SERVER['REQUEST_TIME']); $title = 'Code Coverage Report:'; $output .= $this->format($colors['header'], $padding, $title); $output .= $this->format($colors['header'], $padding, $date); $output .= $this->format($colors['header'], $padding, ''); $output .= $this->format($colors['header'], $padding, ' Summary:'); } $output .= $this->format($colors['classes'], $padding, $classes); $output .= $this->format($colors['methods'], $padding, $methods); $output .= $this->format($colors['lines'], $padding, $lines); if ($this->showOnlySummary) { return $output . PHP_EOL; } $classCoverage = array(); foreach ($report as $item) { if (!$item instanceof PHP_CodeCoverage_Report_Node_File) { continue; } $classes = $item->getClassesAndTraits(); foreach ($classes as $className => $class) { $classStatements = 0; $coveredClassStatements = 0; $coveredMethods = 0; $classMethods = 0; foreach ($class['methods'] as $method) { if ($method['executableLines'] == 0) { continue; } $classMethods++; $classStatements += $method['executableLines']; $coveredClassStatements += $method['executedLines']; if ($method['coverage'] == 100) { $coveredMethods++; } } if (!empty($class['package']['namespace'])) { $namespace = '\\' . $class['package']['namespace'] . '::'; } elseif (!empty($class['package']['fullPackage'])) { $namespace = '@' . $class['package']['fullPackage'] . '::'; } else { $namespace = ''; } $classCoverage[$namespace . $className] = array( 'namespace' => $namespace, 'className ' => $className, 'methodsCovered' => $coveredMethods, 'methodCount' => $classMethods, 'statementsCovered' => $coveredClassStatements, 'statementCount' => $classStatements, ); } } ksort($classCoverage); $methodColor = ''; $linesColor = ''; $resetColor = ''; foreach ($classCoverage as $fullQualifiedPath => $classInfo) { if ($classInfo['statementsCovered'] != 0 || $this->showUncoveredFiles) { if ($showColors) { $methodColor = $this->getCoverageColor($classInfo['methodsCovered'], $classInfo['methodCount']); $linesColor = $this->getCoverageColor($classInfo['statementsCovered'], $classInfo['statementCount']); $resetColor = $colors['reset']; } $output .= PHP_EOL . $fullQualifiedPath . PHP_EOL . ' ' . $methodColor . 'Methods: ' . $this->printCoverageCounts($classInfo['methodsCovered'], $classInfo['methodCount'], 2) . $resetColor . ' ' . ' ' . $linesColor . 'Lines: ' . $this->printCoverageCounts($classInfo['statementsCovered'], $classInfo['statementCount'], 3) . $resetColor ; } } return $output . PHP_EOL; } protected function getCoverageColor($numberOfCoveredElements, $totalNumberOfElements) { $coverage = PHP_CodeCoverage_Util::percent( $numberOfCoveredElements, $totalNumberOfElements ); if ($coverage >= $this->highLowerBound) { return $this->colors['green']; } elseif ($coverage > $this->lowUpperBound) { return $this->colors['yellow']; } return $this->colors['red']; } protected function printCoverageCounts($numberOfCoveredElements, $totalNumberOfElements, $presicion) { $format = '%' . $presicion . 's'; return PHP_CodeCoverage_Util::percent( $numberOfCoveredElements, $totalNumberOfElements, true, true ) . ' (' . sprintf($format, $numberOfCoveredElements) . '/' . sprintf($format, $totalNumberOfElements) . ')'; } private function format($color, $padding, $string) { $reset = $color ? $this->colors['reset'] : ''; return $color . str_pad($string, $padding) . $reset . PHP_EOL; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML_Directory extends PHP_CodeCoverage_Report_XML_Node { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML_File_Coverage { /** * @var XMLWriter */ private $writer; /** * @var DOMElement */ private $contextNode; /** * @var bool */ private $finalized = false; public function __construct(DOMElement $context, $line) { $this->contextNode = $context; $this->writer = new XMLWriter(); $this->writer->openMemory(); $this->writer->startElementNs(null, $context->nodeName, 'http://schema.phpunit.de/coverage/1.0'); $this->writer->writeAttribute('nr', $line); } public function addTest($test) { if ($this->finalized) { throw new PHP_CodeCoverage_Exception('Coverage Report already finalized'); } $this->writer->startElement('covered'); $this->writer->writeAttribute('by', $test); $this->writer->endElement(); } public function finalize() { $this->writer->endElement(); $fragment = $this->contextNode->ownerDocument->createDocumentFragment(); $fragment->appendXML($this->writer->outputMemory()); $this->contextNode->parentNode->replaceChild( $fragment, $this->contextNode ); $this->finalized = true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML_File_Method { /** * @var DOMElement */ private $contextNode; public function __construct(DOMElement $context, $name) { $this->contextNode = $context; $this->setName($name); } private function setName($name) { $this->contextNode->setAttribute('name', $name); } public function setSignature($signature) { $this->contextNode->setAttribute('signature', $signature); } public function setLines($start, $end = null) { $this->contextNode->setAttribute('start', $start); if ($end !== null) { $this->contextNode->setAttribute('end', $end); } } public function setTotals($executable, $executed, $coverage) { $this->contextNode->setAttribute('executable', $executable); $this->contextNode->setAttribute('executed', $executed); $this->contextNode->setAttribute('coverage', $coverage); } public function setCrap($crap) { $this->contextNode->setAttribute('crap', $crap); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML_File_Report extends PHP_CodeCoverage_Report_XML_File { public function __construct($name) { $this->dom = new DOMDocument; $this->dom->loadXML(''); $this->contextNode = $this->dom->getElementsByTagNameNS( 'http://schema.phpunit.de/coverage/1.0', 'file' )->item(0); $this->setName($name); } private function setName($name) { $this->contextNode->setAttribute('name', $name); } public function asDom() { return $this->dom; } public function getFunctionObject($name) { $node = $this->contextNode->appendChild( $this->dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'function' ) ); return new PHP_CodeCoverage_Report_XML_File_Method($node, $name); } public function getClassObject($name) { return $this->getUnitObject('class', $name); } public function getTraitObject($name) { return $this->getUnitObject('trait', $name); } private function getUnitObject($tagName, $name) { $node = $this->contextNode->appendChild( $this->dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', $tagName ) ); return new PHP_CodeCoverage_Report_XML_File_Unit($node, $name); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML_File_Unit { /** * @var DOMElement */ private $contextNode; public function __construct(DOMElement $context, $name) { $this->contextNode = $context; $this->setName($name); } private function setName($name) { $this->contextNode->setAttribute('name', $name); } public function setLines($start, $executable, $executed) { $this->contextNode->setAttribute('start', $start); $this->contextNode->setAttribute('executable', $executable); $this->contextNode->setAttribute('executed', $executed); } public function setCrap($crap) { $this->contextNode->setAttribute('crap', $crap); } public function setPackage($full, $package, $sub, $category) { $node = $this->contextNode->getElementsByTagNameNS( 'http://schema.phpunit.de/coverage/1.0', 'package' )->item(0); if (!$node) { $node = $this->contextNode->appendChild( $this->contextNode->ownerDocument->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'package' ) ); } $node->setAttribute('full', $full); $node->setAttribute('name', $package); $node->setAttribute('sub', $sub); $node->setAttribute('category', $category); } public function setNamespace($namespace) { $node = $this->contextNode->getElementsByTagNameNS( 'http://schema.phpunit.de/coverage/1.0', 'namespace' )->item(0); if (!$node) { $node = $this->contextNode->appendChild( $this->contextNode->ownerDocument->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'namespace' ) ); } $node->setAttribute('name', $namespace); } public function addMethod($name) { $node = $this->contextNode->appendChild( $this->contextNode->ownerDocument->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'method' ) ); return new PHP_CodeCoverage_Report_XML_File_Method($node, $name); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML_File { /** * @var DOMDocument */ protected $dom; /** * @var DOMElement */ protected $contextNode; public function __construct(DOMElement $context) { $this->dom = $context->ownerDocument; $this->contextNode = $context; } public function getTotals() { $totalsContainer = $this->contextNode->firstChild; if (!$totalsContainer) { $totalsContainer = $this->contextNode->appendChild( $this->dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'totals' ) ); } return new PHP_CodeCoverage_Report_XML_Totals($totalsContainer); } public function getLineCoverage($line) { $coverage = $this->contextNode->getElementsByTagNameNS( 'http://schema.phpunit.de/coverage/1.0', 'coverage' )->item(0); if (!$coverage) { $coverage = $this->contextNode->appendChild( $this->dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'coverage' ) ); } $lineNode = $coverage->appendChild( $this->dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'line' ) ); return new PHP_CodeCoverage_Report_XML_File_Coverage($lineNode, $line); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML_Node { /** * @var DOMDocument */ private $dom; /** * @var DOMElement */ private $contextNode; public function __construct(DOMElement $context) { $this->setContextNode($context); } protected function setContextNode(DOMElement $context) { $this->dom = $context->ownerDocument; $this->contextNode = $context; } public function getDom() { return $this->dom; } protected function getContextNode() { return $this->contextNode; } public function getTotals() { $totalsContainer = $this->getContextNode()->firstChild; if (!$totalsContainer) { $totalsContainer = $this->getContextNode()->appendChild( $this->dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'totals' ) ); } return new PHP_CodeCoverage_Report_XML_Totals($totalsContainer); } public function addDirectory($name) { $dirNode = $this->getDom()->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'directory' ); $dirNode->setAttribute('name', $name); $this->getContextNode()->appendChild($dirNode); return new PHP_CodeCoverage_Report_XML_Directory($dirNode); } public function addFile($name, $href) { $fileNode = $this->getDom()->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'file' ); $fileNode->setAttribute('name', $name); $fileNode->setAttribute('href', $href); $this->getContextNode()->appendChild($fileNode); return new PHP_CodeCoverage_Report_XML_File($fileNode); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML_Project extends PHP_CodeCoverage_Report_XML_Node { public function __construct($name) { $this->init(); $this->setProjectName($name); } private function init() { $dom = new DOMDocument; $dom->loadXML(''); $this->setContextNode( $dom->getElementsByTagNameNS( 'http://schema.phpunit.de/coverage/1.0', 'project' )->item(0) ); } private function setProjectName($name) { $this->getContextNode()->setAttribute('name', $name); } public function getTests() { $testsNode = $this->getContextNode()->getElementsByTagNameNS( 'http://schema.phpunit.de/coverage/1.0', 'tests' )->item(0); if (!$testsNode) { $testsNode = $this->getContextNode()->appendChild( $this->getDom()->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'tests' ) ); } return new PHP_CodeCoverage_Report_XML_Tests($testsNode); } public function asDom() { return $this->getDom(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML_Tests { private $contextNode; private $codeMap = array( 0 => 'PASSED', // PHPUnit_Runner_BaseTestRunner::STATUS_PASSED 1 => 'SKIPPED', // PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED 2 => 'INCOMPLETE', // PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE 3 => 'FAILURE', // PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE 4 => 'ERROR', // PHPUnit_Runner_BaseTestRunner::STATUS_ERROR 5 => 'RISKY' // PHPUnit_Runner_BaseTestRunner::STATUS_RISKY ); public function __construct(DOMElement $context) { $this->contextNode = $context; } public function addTest($test, array $result) { $node = $this->contextNode->appendChild( $this->contextNode->ownerDocument->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'test' ) ); $node->setAttribute('name', $test); $node->setAttribute('size', $result['size']); $node->setAttribute('result', (int) $result['status']); $node->setAttribute('status', $this->codeMap[(int) $result['status']]); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML_Totals { /** * @var DOMNode */ private $container; /** * @var DOMElement */ private $linesNode; /** * @var DOMElement */ private $methodsNode; /** * @var DOMElement */ private $functionsNode; /** * @var DOMElement */ private $classesNode; /** * @var DOMElement */ private $traitsNode; public function __construct(DOMElement $container) { $this->container = $container; $dom = $container->ownerDocument; $this->linesNode = $dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'lines' ); $this->methodsNode = $dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'methods' ); $this->functionsNode = $dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'functions' ); $this->classesNode = $dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'classes' ); $this->traitsNode = $dom->createElementNS( 'http://schema.phpunit.de/coverage/1.0', 'traits' ); $container->appendChild($this->linesNode); $container->appendChild($this->methodsNode); $container->appendChild($this->functionsNode); $container->appendChild($this->classesNode); $container->appendChild($this->traitsNode); } public function getContainer() { return $this->container; } public function setNumLines($loc, $cloc, $ncloc, $executable, $executed) { $this->linesNode->setAttribute('total', $loc); $this->linesNode->setAttribute('comments', $cloc); $this->linesNode->setAttribute('code', $ncloc); $this->linesNode->setAttribute('executable', $executable); $this->linesNode->setAttribute('executed', $executed); $this->linesNode->setAttribute( 'percent', PHP_CodeCoverage_Util::percent($executed, $executable, true) ); } public function setNumClasses($count, $tested) { $this->classesNode->setAttribute('count', $count); $this->classesNode->setAttribute('tested', $tested); $this->classesNode->setAttribute( 'percent', PHP_CodeCoverage_Util::percent($tested, $count, true) ); } public function setNumTraits($count, $tested) { $this->traitsNode->setAttribute('count', $count); $this->traitsNode->setAttribute('tested', $tested); $this->traitsNode->setAttribute( 'percent', PHP_CodeCoverage_Util::percent($tested, $count, true) ); } public function setNumMethods($count, $tested) { $this->methodsNode->setAttribute('count', $count); $this->methodsNode->setAttribute('tested', $tested); $this->methodsNode->setAttribute( 'percent', PHP_CodeCoverage_Util::percent($tested, $count, true) ); } public function setNumFunctions($count, $tested) { $this->functionsNode->setAttribute('count', $count); $this->functionsNode->setAttribute('tested', $tested); $this->functionsNode->setAttribute( 'percent', PHP_CodeCoverage_Util::percent($tested, $count, true) ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.0 */ class PHP_CodeCoverage_Report_XML { /** * @var string */ private $target; /** * @var PHP_CodeCoverage_Report_XML_Project */ private $project; public function process(PHP_CodeCoverage $coverage, $target) { if (substr($target, -1, 1) != DIRECTORY_SEPARATOR) { $target .= DIRECTORY_SEPARATOR; } $this->target = $target; $this->initTargetDirectory($target); $report = $coverage->getReport(); $this->project = new PHP_CodeCoverage_Report_XML_Project( $coverage->getReport()->getName() ); $this->processTests($coverage->getTests()); $this->processDirectory($report, $this->project); $index = $this->project->asDom(); $index->formatOutput = true; $index->preserveWhiteSpace = false; $index->save($target . '/index.xml'); } private function initTargetDirectory($dir) { if (file_exists($dir)) { if (!is_dir($dir)) { throw new PHP_CodeCoverage_Exception( "'$dir' exists but is not a directory." ); } if (!is_writable($dir)) { throw new PHP_CodeCoverage_Exception( "'$dir' exists but is not writable." ); } } elseif (!@mkdir($dir, 0777, true)) { throw new PHP_CodeCoverage_Exception( "'$dir' could not be created." ); } } private function processDirectory(PHP_CodeCoverage_Report_Node_Directory $directory, PHP_CodeCoverage_Report_XML_Node $context) { $dirObject = $context->addDirectory($directory->getName()); $this->setTotals($directory, $dirObject->getTotals()); foreach ($directory as $node) { if ($node instanceof PHP_CodeCoverage_Report_Node_Directory) { $this->processDirectory($node, $dirObject); continue; } if ($node instanceof PHP_CodeCoverage_Report_Node_File) { $this->processFile($node, $dirObject); continue; } throw new PHP_CodeCoverage_Exception( 'Unknown node type for XML report' ); } } private function processFile(PHP_CodeCoverage_Report_Node_File $file, PHP_CodeCoverage_Report_XML_Directory $context) { $fileObject = $context->addFile( $file->getName(), $file->getId() . '.xml' ); $this->setTotals($file, $fileObject->getTotals()); $fileReport = new PHP_CodeCoverage_Report_XML_File_Report( $file->getName() ); $this->setTotals($file, $fileReport->getTotals()); foreach ($file->getClassesAndTraits() as $unit) { $this->processUnit($unit, $fileReport); } foreach ($file->getFunctions() as $function) { $this->processFunction($function, $fileReport); } foreach ($file->getCoverageData() as $line => $tests) { if (!is_array($tests) || count($tests) == 0) { continue; } $coverage = $fileReport->getLineCoverage($line); foreach ($tests as $test) { $coverage->addTest($test); } $coverage->finalize(); } $this->initTargetDirectory( $this->target . dirname($file->getId()) . '/' ); $fileDom = $fileReport->asDom(); $fileDom->formatOutput = true; $fileDom->preserveWhiteSpace = false; $fileDom->save($this->target . $file->getId() . '.xml'); } private function processUnit($unit, PHP_CodeCoverage_Report_XML_File_Report $report) { if (isset($unit['className'])) { $unitObject = $report->getClassObject($unit['className']); } else { $unitObject = $report->getTraitObject($unit['traitName']); } $unitObject->setLines( $unit['startLine'], $unit['executableLines'], $unit['executedLines'] ); $unitObject->setCrap($unit['crap']); $unitObject->setPackage( $unit['package']['fullPackage'], $unit['package']['package'], $unit['package']['subpackage'], $unit['package']['category'] ); $unitObject->setNamespace($unit['package']['namespace']); foreach ($unit['methods'] as $method) { $methodObject = $unitObject->addMethod($method['methodName']); $methodObject->setSignature($method['signature']); $methodObject->setLines($method['startLine'], $method['endLine']); $methodObject->setCrap($method['crap']); $methodObject->setTotals( $method['executableLines'], $method['executedLines'], $method['coverage'] ); } } private function processFunction($function, PHP_CodeCoverage_Report_XML_File_Report $report) { $functionObject = $report->getFunctionObject($function['functionName']); $functionObject->setSignature($function['signature']); $functionObject->setLines($function['startLine']); $functionObject->setCrap($function['crap']); $functionObject->setTotals($function['executableLines'], $function['executedLines'], $function['coverage']); } private function processTests(array $tests) { $testsObject = $this->project->getTests(); foreach ($tests as $test => $result) { if ($test == 'UNCOVERED_FILES_FROM_WHITELIST') { continue; } $testsObject->addTest($test, $result); } } private function setTotals(PHP_CodeCoverage_Report_Node $node, PHP_CodeCoverage_Report_XML_Totals $totals) { $loc = $node->getLinesOfCode(); $totals->setNumLines( $loc['loc'], $loc['cloc'], $loc['ncloc'], $node->getNumExecutableLines(), $node->getNumExecutedLines() ); $totals->setNumClasses( $node->getNumClasses(), $node->getNumTestedClasses() ); $totals->setNumTraits( $node->getNumTraits(), $node->getNumTestedTraits() ); $totals->setNumMethods( $node->getNumMethods(), $node->getNumTestedMethods() ); $totals->setNumFunctions( $node->getNumFunctions(), $node->getNumTestedFunctions() ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Factory for PHP_CodeCoverage_Exception objects that are used to describe * invalid arguments passed to a function or method. * * @since Class available since Release 1.2.0 */ class PHP_CodeCoverage_Util_InvalidArgumentHelper { /** * @param int $argument * @param string $type * @param mixed $value */ public static function factory($argument, $type, $value = null) { $stack = debug_backtrace(false); return new PHP_CodeCoverage_Exception( sprintf( 'Argument #%d%sof %s::%s() must be a %s', $argument, $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ', $stack[1]['class'], $stack[1]['function'], $type ) ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Utility methods. * * @since Class available since Release 1.0.0 */ class PHP_CodeCoverage_Util { /** * @param float $a * @param float $b * @return float ($a / $b) * 100 */ public static function percent($a, $b, $asString = false, $fixedWidth = false) { if ($asString && $b == 0) { return ''; } if ($b > 0) { $percent = ($a / $b) * 100; } else { $percent = 100; } if ($asString) { if ($fixedWidth) { return sprintf('%6.2F%%', $percent); } return sprintf('%01.2F%%', $percent); } else { return $percent; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Environment\Runtime; /** * Provides collection functionality for PHP code coverage information. * * @since Class available since Release 1.0.0 */ class PHP_CodeCoverage { /** * @var PHP_CodeCoverage_Driver */ private $driver; /** * @var PHP_CodeCoverage_Filter */ private $filter; /** * @var bool */ private $cacheTokens = false; /** * @var bool */ private $checkForUnintentionallyCoveredCode = false; /** * @var bool */ private $forceCoversAnnotation = false; /** * @var bool */ private $mapTestClassNameToCoveredClassName = false; /** * @var bool */ private $addUncoveredFilesFromWhitelist = true; /** * @var bool */ private $processUncoveredFilesFromWhitelist = false; /** * @var mixed */ private $currentId; /** * Code coverage data. * * @var array */ private $data = array(); /** * @var array */ private $ignoredLines = array(); /** * @var bool */ private $disableIgnoredLines = false; /** * Test data. * * @var array */ private $tests = array(); /** * Constructor. * * @param PHP_CodeCoverage_Driver $driver * @param PHP_CodeCoverage_Filter $filter * @throws PHP_CodeCoverage_Exception */ public function __construct(PHP_CodeCoverage_Driver $driver = null, PHP_CodeCoverage_Filter $filter = null) { if ($driver === null) { $driver = $this->selectDriver(); } if ($filter === null) { $filter = new PHP_CodeCoverage_Filter; } $this->driver = $driver; $this->filter = $filter; } /** * Returns the PHP_CodeCoverage_Report_Node_* object graph * for this PHP_CodeCoverage object. * * @return PHP_CodeCoverage_Report_Node_Directory * @since Method available since Release 1.1.0 */ public function getReport() { $factory = new PHP_CodeCoverage_Report_Factory; return $factory->create($this); } /** * Clears collected code coverage data. */ public function clear() { $this->currentId = null; $this->data = array(); $this->tests = array(); } /** * Returns the PHP_CodeCoverage_Filter used. * * @return PHP_CodeCoverage_Filter */ public function filter() { return $this->filter; } /** * Returns the collected code coverage data. * Set $raw = true to bypass all filters. * * @param bool $raw * @return array * @since Method available since Release 1.1.0 */ public function getData($raw = false) { if (!$raw && $this->addUncoveredFilesFromWhitelist) { $this->addUncoveredFilesFromWhitelist(); } // We need to apply the blacklist filter a second time // when no whitelist is used. if (!$raw && !$this->filter->hasWhitelist()) { $this->applyListsFilter($this->data); } return $this->data; } /** * Sets the coverage data. * * @param array $data * @since Method available since Release 2.0.0 */ public function setData(array $data) { $this->data = $data; } /** * Returns the test data. * * @return array * @since Method available since Release 1.1.0 */ public function getTests() { return $this->tests; } /** * Sets the test data. * * @param array $tests * @since Method available since Release 2.0.0 */ public function setTests(array $tests) { $this->tests = $tests; } /** * Start collection of code coverage information. * * @param mixed $id * @param bool $clear * @throws PHP_CodeCoverage_Exception */ public function start($id, $clear = false) { if (!is_bool($clear)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'boolean' ); } if ($clear) { $this->clear(); } $this->currentId = $id; $this->driver->start(); } /** * Stop collection of code coverage information. * * @param bool $append * @param mixed $linesToBeCovered * @param array $linesToBeUsed * @return array * @throws PHP_CodeCoverage_Exception */ public function stop($append = true, $linesToBeCovered = array(), array $linesToBeUsed = array()) { if (!is_bool($append)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'boolean' ); } if (!is_array($linesToBeCovered) && $linesToBeCovered !== false) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 2, 'array or false' ); } $data = $this->driver->stop(); $this->append($data, null, $append, $linesToBeCovered, $linesToBeUsed); $this->currentId = null; return $data; } /** * Appends code coverage data. * * @param array $data * @param mixed $id * @param bool $append * @param mixed $linesToBeCovered * @param array $linesToBeUsed * @throws PHP_CodeCoverage_Exception */ public function append(array $data, $id = null, $append = true, $linesToBeCovered = array(), array $linesToBeUsed = array()) { if ($id === null) { $id = $this->currentId; } if ($id === null) { throw new PHP_CodeCoverage_Exception; } $this->applyListsFilter($data); $this->applyIgnoredLinesFilter($data); $this->initializeFilesThatAreSeenTheFirstTime($data); if (!$append) { return; } if ($id != 'UNCOVERED_FILES_FROM_WHITELIST') { $this->applyCoversAnnotationFilter( $data, $linesToBeCovered, $linesToBeUsed ); } if (empty($data)) { return; } $size = 'unknown'; $status = null; if ($id instanceof PHPUnit_Framework_TestCase) { $_size = $id->getSize(); if ($_size == PHPUnit_Util_Test::SMALL) { $size = 'small'; } elseif ($_size == PHPUnit_Util_Test::MEDIUM) { $size = 'medium'; } elseif ($_size == PHPUnit_Util_Test::LARGE) { $size = 'large'; } $status = $id->getStatus(); $id = get_class($id) . '::' . $id->getName(); } elseif ($id instanceof PHPUnit_Extensions_PhptTestCase) { $size = 'large'; $id = $id->getName(); } $this->tests[$id] = array('size' => $size, 'status' => $status); foreach ($data as $file => $lines) { if (!$this->filter->isFile($file)) { continue; } foreach ($lines as $k => $v) { if ($v == PHP_CodeCoverage_Driver::LINE_EXECUTED) { if (empty($this->data[$file][$k]) || !in_array($id, $this->data[$file][$k])) { $this->data[$file][$k][] = $id; } } } } } /** * Merges the data from another instance of PHP_CodeCoverage. * * @param PHP_CodeCoverage $that */ public function merge(PHP_CodeCoverage $that) { $this->filter->setBlacklistedFiles( array_merge($this->filter->getBlacklistedFiles(), $that->filter()->getBlacklistedFiles()) ); $this->filter->setWhitelistedFiles( array_merge($this->filter->getWhitelistedFiles(), $that->filter()->getWhitelistedFiles()) ); foreach ($that->data as $file => $lines) { if (!isset($this->data[$file])) { if (!$this->filter->isFiltered($file)) { $this->data[$file] = $lines; } continue; } foreach ($lines as $line => $data) { if ($data !== null) { if (!isset($this->data[$file][$line])) { $this->data[$file][$line] = $data; } else { $this->data[$file][$line] = array_unique( array_merge($this->data[$file][$line], $data) ); } } } } $this->tests = array_merge($this->tests, $that->getTests()); } /** * @param bool $flag * @throws PHP_CodeCoverage_Exception * @since Method available since Release 1.1.0 */ public function setCacheTokens($flag) { if (!is_bool($flag)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'boolean' ); } $this->cacheTokens = $flag; } /** * @since Method available since Release 1.1.0 */ public function getCacheTokens() { return $this->cacheTokens; } /** * @param bool $flag * @throws PHP_CodeCoverage_Exception * @since Method available since Release 2.0.0 */ public function setCheckForUnintentionallyCoveredCode($flag) { if (!is_bool($flag)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'boolean' ); } $this->checkForUnintentionallyCoveredCode = $flag; } /** * @param bool $flag * @throws PHP_CodeCoverage_Exception */ public function setForceCoversAnnotation($flag) { if (!is_bool($flag)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'boolean' ); } $this->forceCoversAnnotation = $flag; } /** * @param bool $flag * @throws PHP_CodeCoverage_Exception */ public function setMapTestClassNameToCoveredClassName($flag) { if (!is_bool($flag)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'boolean' ); } $this->mapTestClassNameToCoveredClassName = $flag; } /** * @param bool $flag * @throws PHP_CodeCoverage_Exception */ public function setAddUncoveredFilesFromWhitelist($flag) { if (!is_bool($flag)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'boolean' ); } $this->addUncoveredFilesFromWhitelist = $flag; } /** * @param bool $flag * @throws PHP_CodeCoverage_Exception */ public function setProcessUncoveredFilesFromWhitelist($flag) { if (!is_bool($flag)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'boolean' ); } $this->processUncoveredFilesFromWhitelist = $flag; } /** * @param bool $flag * @throws PHP_CodeCoverage_Exception */ public function setDisableIgnoredLines($flag) { if (!is_bool($flag)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'boolean' ); } $this->disableIgnoredLines = $flag; } /** * Applies the @covers annotation filtering. * * @param array $data * @param mixed $linesToBeCovered * @param array $linesToBeUsed * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode */ private function applyCoversAnnotationFilter(array &$data, $linesToBeCovered, array $linesToBeUsed) { if ($linesToBeCovered === false || ($this->forceCoversAnnotation && empty($linesToBeCovered))) { $data = array(); return; } if (empty($linesToBeCovered)) { return; } if ($this->checkForUnintentionallyCoveredCode) { $this->performUnintentionallyCoveredCodeCheck( $data, $linesToBeCovered, $linesToBeUsed ); } $data = array_intersect_key($data, $linesToBeCovered); foreach (array_keys($data) as $filename) { $_linesToBeCovered = array_flip($linesToBeCovered[$filename]); $data[$filename] = array_intersect_key( $data[$filename], $_linesToBeCovered ); } } /** * Applies the blacklist/whitelist filtering. * * @param array $data */ private function applyListsFilter(array &$data) { foreach (array_keys($data) as $filename) { if ($this->filter->isFiltered($filename)) { unset($data[$filename]); } } } /** * Applies the "ignored lines" filtering. * * @param array $data */ private function applyIgnoredLinesFilter(array &$data) { foreach (array_keys($data) as $filename) { if (!$this->filter->isFile($filename)) { continue; } foreach ($this->getLinesToBeIgnored($filename) as $line) { unset($data[$filename][$line]); } } } /** * @param array $data * @since Method available since Release 1.1.0 */ private function initializeFilesThatAreSeenTheFirstTime(array $data) { foreach ($data as $file => $lines) { if ($this->filter->isFile($file) && !isset($this->data[$file])) { $this->data[$file] = array(); foreach ($lines as $k => $v) { $this->data[$file][$k] = $v == -2 ? null : array(); } } } } /** * Processes whitelisted files that are not covered. */ private function addUncoveredFilesFromWhitelist() { $data = array(); $uncoveredFiles = array_diff( $this->filter->getWhitelist(), array_keys($this->data) ); foreach ($uncoveredFiles as $uncoveredFile) { if (!file_exists($uncoveredFile)) { continue; } if ($this->processUncoveredFilesFromWhitelist) { $this->processUncoveredFileFromWhitelist( $uncoveredFile, $data, $uncoveredFiles ); } else { $data[$uncoveredFile] = array(); $lines = count(file($uncoveredFile)); for ($i = 1; $i <= $lines; $i++) { $data[$uncoveredFile][$i] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED; } } } $this->append($data, 'UNCOVERED_FILES_FROM_WHITELIST'); } /** * @param string $uncoveredFile * @param array $data * @param array $uncoveredFiles */ private function processUncoveredFileFromWhitelist($uncoveredFile, array &$data, array $uncoveredFiles) { $this->driver->start(); include_once $uncoveredFile; $coverage = $this->driver->stop(); foreach ($coverage as $file => $fileCoverage) { if (!isset($data[$file]) && in_array($file, $uncoveredFiles)) { foreach (array_keys($fileCoverage) as $key) { if ($fileCoverage[$key] == PHP_CodeCoverage_Driver::LINE_EXECUTED) { $fileCoverage[$key] = PHP_CodeCoverage_Driver::LINE_NOT_EXECUTED; } } $data[$file] = $fileCoverage; } } } /** * Returns the lines of a source file that should be ignored. * * @param string $filename * @return array * @throws PHP_CodeCoverage_Exception * @since Method available since Release 2.0.0 */ private function getLinesToBeIgnored($filename) { if (!is_string($filename)) { throw PHP_CodeCoverage_Util_InvalidArgumentHelper::factory( 1, 'string' ); } if (!isset($this->ignoredLines[$filename])) { $this->ignoredLines[$filename] = array(); if ($this->disableIgnoredLines) { return $this->ignoredLines[$filename]; } $ignore = false; $stop = false; $lines = file($filename); $numLines = count($lines); foreach ($lines as $index => $line) { if (!trim($line)) { $this->ignoredLines[$filename][] = $index + 1; } } if ($this->cacheTokens) { $tokens = PHP_Token_Stream_CachingFactory::get($filename); } else { $tokens = new PHP_Token_Stream($filename); } $classes = array_merge($tokens->getClasses(), $tokens->getTraits()); $tokens = $tokens->tokens(); foreach ($tokens as $token) { switch (get_class($token)) { case 'PHP_Token_COMMENT': case 'PHP_Token_DOC_COMMENT': $_token = trim($token); $_line = trim($lines[$token->getLine() - 1]); if ($_token == '// @codeCoverageIgnore' || $_token == '//@codeCoverageIgnore') { $ignore = true; $stop = true; } elseif ($_token == '// @codeCoverageIgnoreStart' || $_token == '//@codeCoverageIgnoreStart') { $ignore = true; } elseif ($_token == '// @codeCoverageIgnoreEnd' || $_token == '//@codeCoverageIgnoreEnd') { $stop = true; } if (!$ignore) { $start = $token->getLine(); $end = $start + substr_count($token, "\n"); // Do not ignore the first line when there is a token // before the comment if (0 !== strpos($_token, $_line)) { $start++; } for ($i = $start; $i < $end; $i++) { $this->ignoredLines[$filename][] = $i; } // A DOC_COMMENT token or a COMMENT token starting with "/*" // does not contain the final \n character in its text if (isset($lines[$i-1]) && 0 === strpos($_token, '/*') && '*/' === substr(trim($lines[$i-1]), -2)) { $this->ignoredLines[$filename][] = $i; } } break; case 'PHP_Token_INTERFACE': case 'PHP_Token_TRAIT': case 'PHP_Token_CLASS': case 'PHP_Token_FUNCTION': $docblock = $token->getDocblock(); $this->ignoredLines[$filename][] = $token->getLine(); if (strpos($docblock, '@codeCoverageIgnore') || strpos($docblock, '@deprecated')) { $endLine = $token->getEndLine(); for ($i = $token->getLine(); $i <= $endLine; $i++) { $this->ignoredLines[$filename][] = $i; } } elseif ($token instanceof PHP_Token_INTERFACE || $token instanceof PHP_Token_TRAIT || $token instanceof PHP_Token_CLASS) { if (empty($classes[$token->getName()]['methods'])) { for ($i = $token->getLine(); $i <= $token->getEndLine(); $i++) { $this->ignoredLines[$filename][] = $i; } } else { $firstMethod = array_shift( $classes[$token->getName()]['methods'] ); do { $lastMethod = array_pop( $classes[$token->getName()]['methods'] ); } while ($lastMethod !== null && substr($lastMethod['signature'], 0, 18) == 'anonymous function'); if ($lastMethod === null) { $lastMethod = $firstMethod; } for ($i = $token->getLine(); $i < $firstMethod['startLine']; $i++) { $this->ignoredLines[$filename][] = $i; } for ($i = $token->getEndLine(); $i > $lastMethod['endLine']; $i--) { $this->ignoredLines[$filename][] = $i; } } } break; case 'PHP_Token_NAMESPACE': $this->ignoredLines[$filename][] = $token->getEndLine(); // Intentional fallthrough case 'PHP_Token_OPEN_TAG': case 'PHP_Token_CLOSE_TAG': case 'PHP_Token_USE': $this->ignoredLines[$filename][] = $token->getLine(); break; } if ($ignore) { $this->ignoredLines[$filename][] = $token->getLine(); if ($stop) { $ignore = false; $stop = false; } } } $this->ignoredLines[$filename][] = $numLines + 1; $this->ignoredLines[$filename] = array_unique( $this->ignoredLines[$filename] ); sort($this->ignoredLines[$filename]); } return $this->ignoredLines[$filename]; } /** * @param array $data * @param array $linesToBeCovered * @param array $linesToBeUsed * @throws PHP_CodeCoverage_Exception_UnintentionallyCoveredCode * @since Method available since Release 2.0.0 */ private function performUnintentionallyCoveredCodeCheck(array &$data, array $linesToBeCovered, array $linesToBeUsed) { $allowedLines = $this->getAllowedLines( $linesToBeCovered, $linesToBeUsed ); $message = ''; foreach ($data as $file => $_data) { foreach ($_data as $line => $flag) { if ($flag == 1 && (!isset($allowedLines[$file]) || !isset($allowedLines[$file][$line]))) { $message .= sprintf( '- %s:%d' . PHP_EOL, $file, $line ); } } } if (!empty($message)) { throw new PHP_CodeCoverage_Exception_UnintentionallyCoveredCode( $message ); } } /** * @param array $linesToBeCovered * @param array $linesToBeUsed * @return array * @since Method available since Release 2.0.0 */ private function getAllowedLines(array $linesToBeCovered, array $linesToBeUsed) { $allowedLines = array(); foreach (array_keys($linesToBeCovered) as $file) { if (!isset($allowedLines[$file])) { $allowedLines[$file] = array(); } $allowedLines[$file] = array_merge( $allowedLines[$file], $linesToBeCovered[$file] ); } foreach (array_keys($linesToBeUsed) as $file) { if (!isset($allowedLines[$file])) { $allowedLines[$file] = array(); } $allowedLines[$file] = array_merge( $allowedLines[$file], $linesToBeUsed[$file] ); } foreach (array_keys($allowedLines) as $file) { $allowedLines[$file] = array_flip( array_unique($allowedLines[$file]) ); } return $allowedLines; } /** * @return PHP_CodeCoverage_Driver * @throws PHP_CodeCoverage_Exception */ private function selectDriver() { $runtime = new Runtime; if (!$runtime->canCollectCodeCoverage()) { throw new PHP_CodeCoverage_Exception('No code coverage driver available'); } if ($runtime->isHHVM()) { return new PHP_CodeCoverage_Driver_HHVM; } elseif ($runtime->isPHPDBG()) { return new PHP_CodeCoverage_Driver_PHPDBG; } else { return new PHP_CodeCoverage_Driver_Xdebug; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Façade implementation that uses File_Iterator_Factory to create a * File_Iterator that operates on an AppendIterator that contains an * RecursiveDirectoryIterator for each given path. The list of unique * files is returned as an array. * * @since Class available since Release 1.3.0 */ class File_Iterator_Facade { /** * @param array|string $paths * @param array|string $suffixes * @param array|string $prefixes * @param array $exclude * @param bool $commonPath * @return array */ public function getFilesAsArray($paths, $suffixes = '', $prefixes = '', array $exclude = array(), $commonPath = FALSE) { if (is_string($paths)) { $paths = array($paths); } $factory = new File_Iterator_Factory; $iterator = $factory->getFileIterator( $paths, $suffixes, $prefixes, $exclude ); $files = array(); foreach ($iterator as $file) { $file = $file->getRealPath(); if ($file) { $files[] = $file; } } foreach ($paths as $path) { if (is_file($path)) { $files[] = realpath($path); } } $files = array_unique($files); sort($files); if ($commonPath) { return array( 'commonPath' => $this->getCommonPath($files), 'files' => $files ); } else { return $files; } } /** * Returns the common path of a set of files. * * @param array $files * @return string */ protected function getCommonPath(array $files) { $count = count($files); if ($count == 0) { return ''; } if ($count == 1) { return dirname($files[0]) . DIRECTORY_SEPARATOR; } $_files = array(); foreach ($files as $file) { $_files[] = $_fileParts = explode(DIRECTORY_SEPARATOR, $file); if (empty($_fileParts[0])) { $_fileParts[0] = DIRECTORY_SEPARATOR; } } $common = ''; $done = FALSE; $j = 0; $count--; while (!$done) { for ($i = 0; $i < $count; $i++) { if ($_files[$i][$j] != $_files[$i+1][$j]) { $done = TRUE; break; } } if (!$done) { $common .= $_files[0][$j]; if ($j > 0) { $common .= DIRECTORY_SEPARATOR; } } $j++; } return DIRECTORY_SEPARATOR . $common; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Factory Method implementation that creates a File_Iterator that operates on * an AppendIterator that contains an RecursiveDirectoryIterator for each given * path. * * @since Class available since Release 1.1.0 */ class File_Iterator_Factory { /** * @param array|string $paths * @param array|string $suffixes * @param array|string $prefixes * @param array $exclude * @return AppendIterator */ public function getFileIterator($paths, $suffixes = '', $prefixes = '', array $exclude = array()) { if (is_string($paths)) { $paths = array($paths); } $paths = $this->getPathsAfterResolvingWildcards($paths); $exclude = $this->getPathsAfterResolvingWildcards($exclude); if (is_string($prefixes)) { if ($prefixes != '') { $prefixes = array($prefixes); } else { $prefixes = array(); } } if (is_string($suffixes)) { if ($suffixes != '') { $suffixes = array($suffixes); } else { $suffixes = array(); } } $iterator = new AppendIterator; foreach ($paths as $path) { if (is_dir($path)) { $iterator->append( new File_Iterator( new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::FOLLOW_SYMLINKS) ), $suffixes, $prefixes, $exclude, $path ) ); } } return $iterator; } /** * @param array $paths * @return array */ protected function getPathsAfterResolvingWildcards(array $paths) { $_paths = array(); foreach ($paths as $path) { if ($locals = glob($path, GLOB_ONLYDIR)) { $_paths = array_merge($_paths, $locals); } else { $_paths[] = $path; } } return $_paths; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * FilterIterator implementation that filters files based on prefix(es) and/or * suffix(es). Hidden files and files from hidden directories are also filtered. * * @since Class available since Release 1.0.0 */ class File_Iterator extends FilterIterator { const PREFIX = 0; const SUFFIX = 1; /** * @var array */ protected $suffixes = array(); /** * @var array */ protected $prefixes = array(); /** * @var array */ protected $exclude = array(); /** * @var string */ protected $basepath; /** * @param Iterator $iterator * @param array $suffixes * @param array $prefixes * @param array $exclude * @param string $basepath */ public function __construct(Iterator $iterator, array $suffixes = array(), array $prefixes = array(), array $exclude = array(), $basepath = NULL) { $exclude = array_filter(array_map('realpath', $exclude)); if ($basepath !== NULL) { $basepath = realpath($basepath); } if ($basepath === FALSE) { $basepath = NULL; } else { foreach ($exclude as &$_exclude) { $_exclude = str_replace($basepath, '', $_exclude); } } $this->prefixes = $prefixes; $this->suffixes = $suffixes; $this->exclude = $exclude; $this->basepath = $basepath; parent::__construct($iterator); } /** * @return bool */ public function accept() { $current = $this->getInnerIterator()->current(); $filename = $current->getFilename(); $realpath = $current->getRealPath(); if ($this->basepath !== NULL) { $realpath = str_replace($this->basepath, '', $realpath); } // Filter files in hidden directories. if (preg_match('=/\.[^/]*/=', $realpath)) { return FALSE; } return $this->acceptPath($realpath) && $this->acceptPrefix($filename) && $this->acceptSuffix($filename); } /** * @param string $path * @return bool * @since Method available since Release 1.1.0 */ protected function acceptPath($path) { foreach ($this->exclude as $exclude) { if (strpos($path, $exclude) === 0) { return FALSE; } } return TRUE; } /** * @param string $filename * @return bool * @since Method available since Release 1.1.0 */ protected function acceptPrefix($filename) { return $this->acceptSubString($filename, $this->prefixes, self::PREFIX); } /** * @param string $filename * @return bool * @since Method available since Release 1.1.0 */ protected function acceptSuffix($filename) { return $this->acceptSubString($filename, $this->suffixes, self::SUFFIX); } /** * @param string $filename * @param array $subString * @param int $type * @return bool * @since Method available since Release 1.1.0 */ protected function acceptSubString($filename, array $subStrings, $type) { if (empty($subStrings)) { return TRUE; } $matched = FALSE; foreach ($subStrings as $string) { if (($type == self::PREFIX && strpos($filename, $string) === 0) || ($type == self::SUFFIX && substr($filename, -1 * strlen($string)) == $string)) { $matched = TRUE; break; } } return $matched; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A simple template engine. * * @since Class available since Release 1.0.0 */ class Text_Template { /** * @var string */ protected $template = ''; /** * @var string */ protected $openDelimiter = '{'; /** * @var string */ protected $closeDelimiter = '}'; /** * @var array */ protected $values = array(); /** * Constructor. * * @param string $file * @throws InvalidArgumentException */ public function __construct($file = '', $openDelimiter = '{', $closeDelimiter = '}') { $this->setFile($file); $this->openDelimiter = $openDelimiter; $this->closeDelimiter = $closeDelimiter; } /** * Sets the template file. * * @param string $file * @throws InvalidArgumentException */ public function setFile($file) { $distFile = $file . '.dist'; if (file_exists($file)) { $this->template = file_get_contents($file); } else if (file_exists($distFile)) { $this->template = file_get_contents($distFile); } else { throw new InvalidArgumentException( 'Template file could not be loaded.' ); } } /** * Sets one or more template variables. * * @param array $values * @param bool $merge */ public function setVar(array $values, $merge = TRUE) { if (!$merge || empty($this->values)) { $this->values = $values; } else { $this->values = array_merge($this->values, $values); } } /** * Renders the template and returns the result. * * @return string */ public function render() { $keys = array(); foreach ($this->values as $key => $value) { $keys[] = $this->openDelimiter . $key . $this->closeDelimiter; } return str_replace($keys, $this->values, $this->template); } /** * Renders the template and writes the result to a file. * * @param string $target */ public function renderTo($target) { $fp = @fopen($target, 'wt'); if ($fp) { fwrite($fp, $this->render()); fclose($fp); } else { $error = error_get_last(); throw new RuntimeException( sprintf( 'Could not write to %s: %s', $target, substr( $error['message'], strpos($error['message'], ':') + 2 ) ) ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Utility class for timing. * * @since Class available since Release 1.0.0 */ class PHP_Timer { /** * @var array */ private static $times = array( 'hour' => 3600000, 'minute' => 60000, 'second' => 1000 ); /** * @var array */ private static $startTimes = array(); /** * @var float */ public static $requestTime; /** * Starts the timer. */ public static function start() { array_push(self::$startTimes, microtime(true)); } /** * Stops the timer and returns the elapsed time. * * @return float */ public static function stop() { return microtime(true) - array_pop(self::$startTimes); } /** * Formats the elapsed time as a string. * * @param float $time * @return string */ public static function secondsToTimeString($time) { $ms = round($time * 1000); foreach (self::$times as $unit => $value) { if ($ms >= $value) { $time = floor($ms / $value * 100.0) / 100.0; return $time . ' ' . ($time == 1 ? $unit : $unit . 's'); } } return $ms . ' ms'; } /** * Formats the elapsed time since the start of the request as a string. * * @return string */ public static function timeSinceStartOfRequest() { return self::secondsToTimeString(microtime(true) - self::$requestTime); } /** * Returns the resources (time, memory) of the request as a string. * * @return string */ public static function resourceUsage() { return sprintf( 'Time: %s, Memory: %4.2fMB', self::timeSinceStartOfRequest(), memory_get_peak_usage(true) / 1048576 ); } } if (isset($_SERVER['REQUEST_TIME_FLOAT'])) { PHP_Timer::$requestTime = $_SERVER['REQUEST_TIME_FLOAT']; } elseif (isset($_SERVER['REQUEST_TIME'])) { PHP_Timer::$requestTime = $_SERVER['REQUEST_TIME']; } else { PHP_Timer::$requestTime = microtime(true); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A caching factory for token stream objects. * * @author Sebastian Bergmann * @copyright Sebastian Bergmann * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License * @link http://github.com/sebastianbergmann/php-token-stream/tree * @since Class available since Release 1.0.0 */ class PHP_Token_Stream_CachingFactory { /** * @var array */ protected static $cache = array(); /** * @param string $filename * @return PHP_Token_Stream */ public static function get($filename) { if (!isset(self::$cache[$filename])) { self::$cache[$filename] = new PHP_Token_Stream($filename); } return self::$cache[$filename]; } /** * @param string $filename */ public static function clear($filename = null) { if (is_string($filename)) { unset(self::$cache[$filename]); } else { self::$cache = array(); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A stream of PHP tokens. * * @author Sebastian Bergmann * @copyright Sebastian Bergmann * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License * @link http://github.com/sebastianbergmann/php-token-stream/tree * @since Class available since Release 1.0.0 */ class PHP_Token_Stream implements ArrayAccess, Countable, SeekableIterator { /** * @var array */ protected static $customTokens = array( '(' => 'PHP_Token_OPEN_BRACKET', ')' => 'PHP_Token_CLOSE_BRACKET', '[' => 'PHP_Token_OPEN_SQUARE', ']' => 'PHP_Token_CLOSE_SQUARE', '{' => 'PHP_Token_OPEN_CURLY', '}' => 'PHP_Token_CLOSE_CURLY', ';' => 'PHP_Token_SEMICOLON', '.' => 'PHP_Token_DOT', ',' => 'PHP_Token_COMMA', '=' => 'PHP_Token_EQUAL', '<' => 'PHP_Token_LT', '>' => 'PHP_Token_GT', '+' => 'PHP_Token_PLUS', '-' => 'PHP_Token_MINUS', '*' => 'PHP_Token_MULT', '/' => 'PHP_Token_DIV', '?' => 'PHP_Token_QUESTION_MARK', '!' => 'PHP_Token_EXCLAMATION_MARK', ':' => 'PHP_Token_COLON', '"' => 'PHP_Token_DOUBLE_QUOTES', '@' => 'PHP_Token_AT', '&' => 'PHP_Token_AMPERSAND', '%' => 'PHP_Token_PERCENT', '|' => 'PHP_Token_PIPE', '$' => 'PHP_Token_DOLLAR', '^' => 'PHP_Token_CARET', '~' => 'PHP_Token_TILDE', '`' => 'PHP_Token_BACKTICK' ); /** * @var string */ protected $filename; /** * @var array */ protected $tokens = array(); /** * @var integer */ protected $position = 0; /** * @var array */ protected $linesOfCode = array('loc' => 0, 'cloc' => 0, 'ncloc' => 0); /** * @var array */ protected $classes; /** * @var array */ protected $functions; /** * @var array */ protected $includes; /** * @var array */ protected $interfaces; /** * @var array */ protected $traits; /** * @var array */ protected $lineToFunctionMap = array(); /** * Constructor. * * @param string $sourceCode */ public function __construct($sourceCode) { if (is_file($sourceCode)) { $this->filename = $sourceCode; $sourceCode = file_get_contents($sourceCode); } $this->scan($sourceCode); } /** * Destructor. */ public function __destruct() { $this->tokens = array(); } /** * @return string */ public function __toString() { $buffer = ''; foreach ($this as $token) { $buffer .= $token; } return $buffer; } /** * @return string * @since Method available since Release 1.1.0 */ public function getFilename() { return $this->filename; } /** * Scans the source for sequences of characters and converts them into a * stream of tokens. * * @param string $sourceCode */ protected function scan($sourceCode) { $line = 1; $tokens = token_get_all($sourceCode); $numTokens = count($tokens); $lastNonWhitespaceTokenWasDoubleColon = false; for ($i = 0; $i < $numTokens; ++$i) { $token = $tokens[$i]; unset($tokens[$i]); if (is_array($token)) { $name = substr(token_name($token[0]), 2); $text = $token[1]; if ($lastNonWhitespaceTokenWasDoubleColon && $name == 'CLASS') { $name = 'CLASS_NAME_CONSTANT'; } elseif ($name == 'USE' && isset($tokens[$i+2][0]) && $tokens[$i+2][0] == T_FUNCTION) { unset($tokens[$i+1]); unset($tokens[$i+2]); $i += 2; $name = 'USE_FUNCTION'; } $tokenClass = 'PHP_Token_' . $name; } else { $text = $token; $tokenClass = self::$customTokens[$token]; } $this->tokens[] = new $tokenClass($text, $line, $this, $i); $lines = substr_count($text, "\n"); $line += $lines; if ($tokenClass == 'PHP_Token_HALT_COMPILER') { break; } elseif ($tokenClass == 'PHP_Token_COMMENT' || $tokenClass == 'PHP_Token_DOC_COMMENT') { $this->linesOfCode['cloc'] += $lines + 1; } if ($name == 'DOUBLE_COLON') { $lastNonWhitespaceTokenWasDoubleColon = true; } elseif ($name != 'WHITESPACE') { $lastNonWhitespaceTokenWasDoubleColon = false; } } $this->linesOfCode['loc'] = substr_count($sourceCode, "\n"); $this->linesOfCode['ncloc'] = $this->linesOfCode['loc'] - $this->linesOfCode['cloc']; } /** * @return integer */ public function count() { return count($this->tokens); } /** * @return PHP_Token[] */ public function tokens() { return $this->tokens; } /** * @return array */ public function getClasses() { if ($this->classes !== null) { return $this->classes; } $this->parse(); return $this->classes; } /** * @return array */ public function getFunctions() { if ($this->functions !== null) { return $this->functions; } $this->parse(); return $this->functions; } /** * @return array */ public function getInterfaces() { if ($this->interfaces !== null) { return $this->interfaces; } $this->parse(); return $this->interfaces; } /** * @return array * @since Method available since Release 1.1.0 */ public function getTraits() { if ($this->traits !== null) { return $this->traits; } $this->parse(); return $this->traits; } /** * Gets the names of all files that have been included * using include(), include_once(), require() or require_once(). * * Parameter $categorize set to TRUE causing this function to return a * multi-dimensional array with categories in the keys of the first dimension * and constants and their values in the second dimension. * * Parameter $category allow to filter following specific inclusion type * * @param bool $categorize OPTIONAL * @param string $category OPTIONAL Either 'require_once', 'require', * 'include_once', 'include'. * @return array * @since Method available since Release 1.1.0 */ public function getIncludes($categorize = false, $category = null) { if ($this->includes === null) { $this->includes = array( 'require_once' => array(), 'require' => array(), 'include_once' => array(), 'include' => array() ); foreach ($this->tokens as $token) { switch (get_class($token)) { case 'PHP_Token_REQUIRE_ONCE': case 'PHP_Token_REQUIRE': case 'PHP_Token_INCLUDE_ONCE': case 'PHP_Token_INCLUDE': $this->includes[$token->getType()][] = $token->getName(); break; } } } if (isset($this->includes[$category])) { $includes = $this->includes[$category]; } elseif ($categorize === false) { $includes = array_merge( $this->includes['require_once'], $this->includes['require'], $this->includes['include_once'], $this->includes['include'] ); } else { $includes = $this->includes; } return $includes; } /** * Returns the name of the function or method a line belongs to. * * @return string or null if the line is not in a function or method * @since Method available since Release 1.2.0 */ public function getFunctionForLine($line) { $this->parse(); if (isset($this->lineToFunctionMap[$line])) { return $this->lineToFunctionMap[$line]; } } protected function parse() { $this->interfaces = array(); $this->classes = array(); $this->traits = array(); $this->functions = array(); $class = array(); $classEndLine = array(); $trait = false; $traitEndLine = false; $interface = false; $interfaceEndLine = false; foreach ($this->tokens as $token) { switch (get_class($token)) { case 'PHP_Token_HALT_COMPILER': return; case 'PHP_Token_INTERFACE': $interface = $token->getName(); $interfaceEndLine = $token->getEndLine(); $this->interfaces[$interface] = array( 'methods' => array(), 'parent' => $token->getParent(), 'keywords' => $token->getKeywords(), 'docblock' => $token->getDocblock(), 'startLine' => $token->getLine(), 'endLine' => $interfaceEndLine, 'package' => $token->getPackage(), 'file' => $this->filename ); break; case 'PHP_Token_CLASS': case 'PHP_Token_TRAIT': $tmp = array( 'methods' => array(), 'parent' => $token->getParent(), 'interfaces'=> $token->getInterfaces(), 'keywords' => $token->getKeywords(), 'docblock' => $token->getDocblock(), 'startLine' => $token->getLine(), 'endLine' => $token->getEndLine(), 'package' => $token->getPackage(), 'file' => $this->filename ); if ($token instanceof PHP_Token_CLASS) { $class[] = $token->getName(); $classEndLine[] = $token->getEndLine(); if ($class[count($class)-1] != 'anonymous class') { $this->classes[$class[count($class)-1]] = $tmp; } } else { $trait = $token->getName(); $traitEndLine = $token->getEndLine(); $this->traits[$trait] = $tmp; } break; case 'PHP_Token_FUNCTION': $name = $token->getName(); $tmp = array( 'docblock' => $token->getDocblock(), 'keywords' => $token->getKeywords(), 'visibility'=> $token->getVisibility(), 'signature' => $token->getSignature(), 'startLine' => $token->getLine(), 'endLine' => $token->getEndLine(), 'ccn' => $token->getCCN(), 'file' => $this->filename ); if (empty($class) && $trait === false && $interface === false) { $this->functions[$name] = $tmp; $this->addFunctionToMap( $name, $tmp['startLine'], $tmp['endLine'] ); } elseif (!empty($class) && $class[count($class)-1] != 'anonymous class') { $this->classes[$class[count($class)-1]]['methods'][$name] = $tmp; $this->addFunctionToMap( $class[count($class)-1] . '::' . $name, $tmp['startLine'], $tmp['endLine'] ); } elseif ($trait !== false) { $this->traits[$trait]['methods'][$name] = $tmp; $this->addFunctionToMap( $trait . '::' . $name, $tmp['startLine'], $tmp['endLine'] ); } else { $this->interfaces[$interface]['methods'][$name] = $tmp; } break; case 'PHP_Token_CLOSE_CURLY': if (!empty($classEndLine) && $classEndLine[count($classEndLine)-1] == $token->getLine()) { array_pop($classEndLine); array_pop($class); } elseif ($traitEndLine !== false && $traitEndLine == $token->getLine()) { $trait = false; $traitEndLine = false; } elseif ($interfaceEndLine !== false && $interfaceEndLine == $token->getLine()) { $interface = false; $interfaceEndLine = false; } break; } } } /** * @return array */ public function getLinesOfCode() { return $this->linesOfCode; } /** */ public function rewind() { $this->position = 0; } /** * @return boolean */ public function valid() { return isset($this->tokens[$this->position]); } /** * @return integer */ public function key() { return $this->position; } /** * @return PHP_Token */ public function current() { return $this->tokens[$this->position]; } /** */ public function next() { $this->position++; } /** * @param integer $offset * @return boolean */ public function offsetExists($offset) { return isset($this->tokens[$offset]); } /** * @param integer $offset * @return mixed * @throws OutOfBoundsException */ public function offsetGet($offset) { if (!$this->offsetExists($offset)) { throw new OutOfBoundsException( sprintf( 'No token at position "%s"', $offset ) ); } return $this->tokens[$offset]; } /** * @param integer $offset * @param mixed $value */ public function offsetSet($offset, $value) { $this->tokens[$offset] = $value; } /** * @param integer $offset * @throws OutOfBoundsException */ public function offsetUnset($offset) { if (!$this->offsetExists($offset)) { throw new OutOfBoundsException( sprintf( 'No token at position "%s"', $offset ) ); } unset($this->tokens[$offset]); } /** * Seek to an absolute position. * * @param integer $position * @throws OutOfBoundsException */ public function seek($position) { $this->position = $position; if (!$this->valid()) { throw new OutOfBoundsException( sprintf( 'No token at position "%s"', $this->position ) ); } } /** * @param string $name * @param integer $startLine * @param integer $endLine */ private function addFunctionToMap($name, $startLine, $endLine) { for ($line = $startLine; $line <= $endLine; $line++) { $this->lineToFunctionMap[$line] = $name; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A PHP token. * * @author Sebastian Bergmann * @copyright Sebastian Bergmann * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License * @link http://github.com/sebastianbergmann/php-token-stream/tree * @since Class available since Release 1.0.0 */ abstract class PHP_Token { /** * @var string */ protected $text; /** * @var integer */ protected $line; /** * @var PHP_Token_Stream */ protected $tokenStream; /** * @var integer */ protected $id; /** * Constructor. * * @param string $text * @param integer $line * @param PHP_Token_Stream $tokenStream * @param integer $id */ public function __construct($text, $line, PHP_Token_Stream $tokenStream, $id) { $this->text = $text; $this->line = $line; $this->tokenStream = $tokenStream; $this->id = $id; } /** * @return string */ public function __toString() { return $this->text; } /** * @return integer */ public function getLine() { return $this->line; } } abstract class PHP_TokenWithScope extends PHP_Token { /** * @var integer */ protected $endTokenId; /** * Get the docblock for this token * * This method will fetch the docblock belonging to the current token. The * docblock must be placed on the line directly above the token to be * recognized. * * @return string|null Returns the docblock as a string if found */ public function getDocblock() { $tokens = $this->tokenStream->tokens(); $currentLineNumber = $tokens[$this->id]->getLine(); $prevLineNumber = $currentLineNumber - 1; for ($i = $this->id - 1; $i; $i--) { if (!isset($tokens[$i])) { return; } if ($tokens[$i] instanceof PHP_Token_FUNCTION || $tokens[$i] instanceof PHP_Token_CLASS || $tokens[$i] instanceof PHP_Token_TRAIT) { // Some other trait, class or function, no docblock can be // used for the current token break; } $line = $tokens[$i]->getLine(); if ($line == $currentLineNumber || ($line == $prevLineNumber && $tokens[$i] instanceof PHP_Token_WHITESPACE)) { continue; } if ($line < $currentLineNumber && !$tokens[$i] instanceof PHP_Token_DOC_COMMENT) { break; } return (string)$tokens[$i]; } } /** * @return integer */ public function getEndTokenId() { $block = 0; $i = $this->id; $tokens = $this->tokenStream->tokens(); while ($this->endTokenId === null && isset($tokens[$i])) { if ($tokens[$i] instanceof PHP_Token_OPEN_CURLY || $tokens[$i] instanceof PHP_Token_CURLY_OPEN) { $block++; } elseif ($tokens[$i] instanceof PHP_Token_CLOSE_CURLY) { $block--; if ($block === 0) { $this->endTokenId = $i; } } elseif (($this instanceof PHP_Token_FUNCTION || $this instanceof PHP_Token_NAMESPACE) && $tokens[$i] instanceof PHP_Token_SEMICOLON) { if ($block === 0) { $this->endTokenId = $i; } } $i++; } if ($this->endTokenId === null) { $this->endTokenId = $this->id; } return $this->endTokenId; } /** * @return integer */ public function getEndLine() { return $this->tokenStream[$this->getEndTokenId()]->getLine(); } } abstract class PHP_TokenWithScopeAndVisibility extends PHP_TokenWithScope { /** * @return string */ public function getVisibility() { $tokens = $this->tokenStream->tokens(); for ($i = $this->id - 2; $i > $this->id - 7; $i -= 2) { if (isset($tokens[$i]) && ($tokens[$i] instanceof PHP_Token_PRIVATE || $tokens[$i] instanceof PHP_Token_PROTECTED || $tokens[$i] instanceof PHP_Token_PUBLIC)) { return strtolower( str_replace('PHP_Token_', '', get_class($tokens[$i])) ); } if (isset($tokens[$i]) && !($tokens[$i] instanceof PHP_Token_STATIC || $tokens[$i] instanceof PHP_Token_FINAL || $tokens[$i] instanceof PHP_Token_ABSTRACT)) { // no keywords; stop visibility search break; } } } /** * @return string */ public function getKeywords() { $keywords = array(); $tokens = $this->tokenStream->tokens(); for ($i = $this->id - 2; $i > $this->id - 7; $i -= 2) { if (isset($tokens[$i]) && ($tokens[$i] instanceof PHP_Token_PRIVATE || $tokens[$i] instanceof PHP_Token_PROTECTED || $tokens[$i] instanceof PHP_Token_PUBLIC)) { continue; } if (isset($tokens[$i]) && ($tokens[$i] instanceof PHP_Token_STATIC || $tokens[$i] instanceof PHP_Token_FINAL || $tokens[$i] instanceof PHP_Token_ABSTRACT)) { $keywords[] = strtolower( str_replace('PHP_Token_', '', get_class($tokens[$i])) ); } } return implode(',', $keywords); } } abstract class PHP_Token_Includes extends PHP_Token { /** * @var string */ protected $name; /** * @var string */ protected $type; /** * @return string */ public function getName() { if ($this->name === null) { $this->process(); } return $this->name; } /** * @return string */ public function getType() { if ($this->type === null) { $this->process(); } return $this->type; } private function process() { $tokens = $this->tokenStream->tokens(); if ($tokens[$this->id+2] instanceof PHP_Token_CONSTANT_ENCAPSED_STRING) { $this->name = trim($tokens[$this->id+2], "'\""); $this->type = strtolower( str_replace('PHP_Token_', '', get_class($tokens[$this->id])) ); } } } class PHP_Token_FUNCTION extends PHP_TokenWithScopeAndVisibility { /** * @var array */ protected $arguments; /** * @var integer */ protected $ccn; /** * @var string */ protected $name; /** * @var string */ protected $signature; /** * @return array */ public function getArguments() { if ($this->arguments !== null) { return $this->arguments; } $this->arguments = array(); $tokens = $this->tokenStream->tokens(); $typeDeclaration = null; // Search for first token inside brackets $i = $this->id + 2; while (!$tokens[$i-1] instanceof PHP_Token_OPEN_BRACKET) { $i++; } while (!$tokens[$i] instanceof PHP_Token_CLOSE_BRACKET) { if ($tokens[$i] instanceof PHP_Token_STRING) { $typeDeclaration = (string)$tokens[$i]; } elseif ($tokens[$i] instanceof PHP_Token_VARIABLE) { $this->arguments[(string)$tokens[$i]] = $typeDeclaration; $typeDeclaration = null; } $i++; } return $this->arguments; } /** * @return string */ public function getName() { if ($this->name !== null) { return $this->name; } $tokens = $this->tokenStream->tokens(); for ($i = $this->id + 1; $i < count($tokens); $i++) { if ($tokens[$i] instanceof PHP_Token_STRING) { $this->name = (string)$tokens[$i]; break; } elseif ($tokens[$i] instanceof PHP_Token_AMPERSAND && $tokens[$i+1] instanceof PHP_Token_STRING) { $this->name = (string)$tokens[$i+1]; break; } elseif ($tokens[$i] instanceof PHP_Token_OPEN_BRACKET) { $this->name = 'anonymous function'; break; } } if ($this->name != 'anonymous function') { for ($i = $this->id; $i; --$i) { if ($tokens[$i] instanceof PHP_Token_NAMESPACE) { $this->name = $tokens[$i]->getName() . '\\' . $this->name; break; } if ($tokens[$i] instanceof PHP_Token_INTERFACE) { break; } } } return $this->name; } /** * @return integer */ public function getCCN() { if ($this->ccn !== null) { return $this->ccn; } $this->ccn = 1; $end = $this->getEndTokenId(); $tokens = $this->tokenStream->tokens(); for ($i = $this->id; $i <= $end; $i++) { switch (get_class($tokens[$i])) { case 'PHP_Token_IF': case 'PHP_Token_ELSEIF': case 'PHP_Token_FOR': case 'PHP_Token_FOREACH': case 'PHP_Token_WHILE': case 'PHP_Token_CASE': case 'PHP_Token_CATCH': case 'PHP_Token_BOOLEAN_AND': case 'PHP_Token_LOGICAL_AND': case 'PHP_Token_BOOLEAN_OR': case 'PHP_Token_LOGICAL_OR': case 'PHP_Token_QUESTION_MARK': $this->ccn++; break; } } return $this->ccn; } /** * @return string */ public function getSignature() { if ($this->signature !== null) { return $this->signature; } if ($this->getName() == 'anonymous function') { $this->signature = 'anonymous function'; $i = $this->id + 1; } else { $this->signature = ''; $i = $this->id + 2; } $tokens = $this->tokenStream->tokens(); while (isset($tokens[$i]) && !$tokens[$i] instanceof PHP_Token_OPEN_CURLY && !$tokens[$i] instanceof PHP_Token_SEMICOLON) { $this->signature .= $tokens[$i++]; } $this->signature = trim($this->signature); return $this->signature; } } class PHP_Token_INTERFACE extends PHP_TokenWithScopeAndVisibility { /** * @var array */ protected $interfaces; /** * @return string */ public function getName() { return (string)$this->tokenStream[$this->id + 2]; } /** * @return boolean */ public function hasParent() { return $this->tokenStream[$this->id + 4] instanceof PHP_Token_EXTENDS; } /** * @return array */ public function getPackage() { $className = $this->getName(); $docComment = $this->getDocblock(); $result = array( 'namespace' => '', 'fullPackage' => '', 'category' => '', 'package' => '', 'subpackage' => '' ); for ($i = $this->id; $i; --$i) { if ($this->tokenStream[$i] instanceof PHP_Token_NAMESPACE) { $result['namespace'] = $this->tokenStream[$i]->getName(); break; } } if (preg_match('/@category[\s]+([\.\w]+)/', $docComment, $matches)) { $result['category'] = $matches[1]; } if (preg_match('/@package[\s]+([\.\w]+)/', $docComment, $matches)) { $result['package'] = $matches[1]; $result['fullPackage'] = $matches[1]; } if (preg_match('/@subpackage[\s]+([\.\w]+)/', $docComment, $matches)) { $result['subpackage'] = $matches[1]; $result['fullPackage'] .= '.' . $matches[1]; } if (empty($result['fullPackage'])) { $result['fullPackage'] = $this->arrayToName( explode('_', str_replace('\\', '_', $className)), '.' ); } return $result; } /** * @param array $parts * @param string $join * @return string */ protected function arrayToName(array $parts, $join = '\\') { $result = ''; if (count($parts) > 1) { array_pop($parts); $result = join($join, $parts); } return $result; } /** * @return boolean|string */ public function getParent() { if (!$this->hasParent()) { return false; } $i = $this->id + 6; $tokens = $this->tokenStream->tokens(); $className = (string)$tokens[$i]; while (isset($tokens[$i+1]) && !$tokens[$i+1] instanceof PHP_Token_WHITESPACE) { $className .= (string)$tokens[++$i]; } return $className; } /** * @return boolean */ public function hasInterfaces() { return (isset($this->tokenStream[$this->id + 4]) && $this->tokenStream[$this->id + 4] instanceof PHP_Token_IMPLEMENTS) || (isset($this->tokenStream[$this->id + 8]) && $this->tokenStream[$this->id + 8] instanceof PHP_Token_IMPLEMENTS); } /** * @return array|boolean */ public function getInterfaces() { if ($this->interfaces !== null) { return $this->interfaces; } if (!$this->hasInterfaces()) { return ($this->interfaces = false); } if ($this->tokenStream[$this->id + 4] instanceof PHP_Token_IMPLEMENTS) { $i = $this->id + 3; } else { $i = $this->id + 7; } $tokens = $this->tokenStream->tokens(); while (!$tokens[$i+1] instanceof PHP_Token_OPEN_CURLY) { $i++; if ($tokens[$i] instanceof PHP_Token_STRING) { $this->interfaces[] = (string)$tokens[$i]; } } return $this->interfaces; } } class PHP_Token_ABSTRACT extends PHP_Token {} class PHP_Token_AMPERSAND extends PHP_Token {} class PHP_Token_AND_EQUAL extends PHP_Token {} class PHP_Token_ARRAY extends PHP_Token {} class PHP_Token_ARRAY_CAST extends PHP_Token {} class PHP_Token_AS extends PHP_Token {} class PHP_Token_AT extends PHP_Token {} class PHP_Token_BACKTICK extends PHP_Token {} class PHP_Token_BAD_CHARACTER extends PHP_Token {} class PHP_Token_BOOLEAN_AND extends PHP_Token {} class PHP_Token_BOOLEAN_OR extends PHP_Token {} class PHP_Token_BOOL_CAST extends PHP_Token {} class PHP_Token_BREAK extends PHP_Token {} class PHP_Token_CARET extends PHP_Token {} class PHP_Token_CASE extends PHP_Token {} class PHP_Token_CATCH extends PHP_Token {} class PHP_Token_CHARACTER extends PHP_Token {} class PHP_Token_CLASS extends PHP_Token_INTERFACE { /** * @return string */ public function getName() { $next = $this->tokenStream[$this->id + 1]; if ($next instanceof PHP_Token_WHITESPACE) { $next = $this->tokenStream[$this->id + 2]; } if ($next instanceof PHP_Token_STRING) { return (string) $next; } if ($next instanceof PHP_Token_OPEN_CURLY || $next instanceof PHP_Token_EXTENDS || $next instanceof PHP_Token_IMPLEMENTS) { return 'anonymous class'; } } } class PHP_Token_CLASS_C extends PHP_Token {} class PHP_Token_CLASS_NAME_CONSTANT extends PHP_Token {} class PHP_Token_CLONE extends PHP_Token {} class PHP_Token_CLOSE_BRACKET extends PHP_Token {} class PHP_Token_CLOSE_CURLY extends PHP_Token {} class PHP_Token_CLOSE_SQUARE extends PHP_Token {} class PHP_Token_CLOSE_TAG extends PHP_Token {} class PHP_Token_COLON extends PHP_Token {} class PHP_Token_COMMA extends PHP_Token {} class PHP_Token_COMMENT extends PHP_Token {} class PHP_Token_CONCAT_EQUAL extends PHP_Token {} class PHP_Token_CONST extends PHP_Token {} class PHP_Token_CONSTANT_ENCAPSED_STRING extends PHP_Token {} class PHP_Token_CONTINUE extends PHP_Token {} class PHP_Token_CURLY_OPEN extends PHP_Token {} class PHP_Token_DEC extends PHP_Token {} class PHP_Token_DECLARE extends PHP_Token {} class PHP_Token_DEFAULT extends PHP_Token {} class PHP_Token_DIV extends PHP_Token {} class PHP_Token_DIV_EQUAL extends PHP_Token {} class PHP_Token_DNUMBER extends PHP_Token {} class PHP_Token_DO extends PHP_Token {} class PHP_Token_DOC_COMMENT extends PHP_Token {} class PHP_Token_DOLLAR extends PHP_Token {} class PHP_Token_DOLLAR_OPEN_CURLY_BRACES extends PHP_Token {} class PHP_Token_DOT extends PHP_Token {} class PHP_Token_DOUBLE_ARROW extends PHP_Token {} class PHP_Token_DOUBLE_CAST extends PHP_Token {} class PHP_Token_DOUBLE_COLON extends PHP_Token {} class PHP_Token_DOUBLE_QUOTES extends PHP_Token {} class PHP_Token_ECHO extends PHP_Token {} class PHP_Token_ELSE extends PHP_Token {} class PHP_Token_ELSEIF extends PHP_Token {} class PHP_Token_EMPTY extends PHP_Token {} class PHP_Token_ENCAPSED_AND_WHITESPACE extends PHP_Token {} class PHP_Token_ENDDECLARE extends PHP_Token {} class PHP_Token_ENDFOR extends PHP_Token {} class PHP_Token_ENDFOREACH extends PHP_Token {} class PHP_Token_ENDIF extends PHP_Token {} class PHP_Token_ENDSWITCH extends PHP_Token {} class PHP_Token_ENDWHILE extends PHP_Token {} class PHP_Token_END_HEREDOC extends PHP_Token {} class PHP_Token_EQUAL extends PHP_Token {} class PHP_Token_EVAL extends PHP_Token {} class PHP_Token_EXCLAMATION_MARK extends PHP_Token {} class PHP_Token_EXIT extends PHP_Token {} class PHP_Token_EXTENDS extends PHP_Token {} class PHP_Token_FILE extends PHP_Token {} class PHP_Token_FINAL extends PHP_Token {} class PHP_Token_FOR extends PHP_Token {} class PHP_Token_FOREACH extends PHP_Token {} class PHP_Token_FUNC_C extends PHP_Token {} class PHP_Token_GLOBAL extends PHP_Token {} class PHP_Token_GT extends PHP_Token {} class PHP_Token_IF extends PHP_Token {} class PHP_Token_IMPLEMENTS extends PHP_Token {} class PHP_Token_INC extends PHP_Token {} class PHP_Token_INCLUDE extends PHP_Token_Includes {} class PHP_Token_INCLUDE_ONCE extends PHP_Token_Includes {} class PHP_Token_INLINE_HTML extends PHP_Token {} class PHP_Token_INSTANCEOF extends PHP_Token {} class PHP_Token_INT_CAST extends PHP_Token {} class PHP_Token_ISSET extends PHP_Token {} class PHP_Token_IS_EQUAL extends PHP_Token {} class PHP_Token_IS_GREATER_OR_EQUAL extends PHP_Token {} class PHP_Token_IS_IDENTICAL extends PHP_Token {} class PHP_Token_IS_NOT_EQUAL extends PHP_Token {} class PHP_Token_IS_NOT_IDENTICAL extends PHP_Token {} class PHP_Token_IS_SMALLER_OR_EQUAL extends PHP_Token {} class PHP_Token_LINE extends PHP_Token {} class PHP_Token_LIST extends PHP_Token {} class PHP_Token_LNUMBER extends PHP_Token {} class PHP_Token_LOGICAL_AND extends PHP_Token {} class PHP_Token_LOGICAL_OR extends PHP_Token {} class PHP_Token_LOGICAL_XOR extends PHP_Token {} class PHP_Token_LT extends PHP_Token {} class PHP_Token_METHOD_C extends PHP_Token {} class PHP_Token_MINUS extends PHP_Token {} class PHP_Token_MINUS_EQUAL extends PHP_Token {} class PHP_Token_MOD_EQUAL extends PHP_Token {} class PHP_Token_MULT extends PHP_Token {} class PHP_Token_MUL_EQUAL extends PHP_Token {} class PHP_Token_NEW extends PHP_Token {} class PHP_Token_NUM_STRING extends PHP_Token {} class PHP_Token_OBJECT_CAST extends PHP_Token {} class PHP_Token_OBJECT_OPERATOR extends PHP_Token {} class PHP_Token_OPEN_BRACKET extends PHP_Token {} class PHP_Token_OPEN_CURLY extends PHP_Token {} class PHP_Token_OPEN_SQUARE extends PHP_Token {} class PHP_Token_OPEN_TAG extends PHP_Token {} class PHP_Token_OPEN_TAG_WITH_ECHO extends PHP_Token {} class PHP_Token_OR_EQUAL extends PHP_Token {} class PHP_Token_PAAMAYIM_NEKUDOTAYIM extends PHP_Token {} class PHP_Token_PERCENT extends PHP_Token {} class PHP_Token_PIPE extends PHP_Token {} class PHP_Token_PLUS extends PHP_Token {} class PHP_Token_PLUS_EQUAL extends PHP_Token {} class PHP_Token_PRINT extends PHP_Token {} class PHP_Token_PRIVATE extends PHP_Token {} class PHP_Token_PROTECTED extends PHP_Token {} class PHP_Token_PUBLIC extends PHP_Token {} class PHP_Token_QUESTION_MARK extends PHP_Token {} class PHP_Token_REQUIRE extends PHP_Token_Includes {} class PHP_Token_REQUIRE_ONCE extends PHP_Token_Includes {} class PHP_Token_RETURN extends PHP_Token {} class PHP_Token_SEMICOLON extends PHP_Token {} class PHP_Token_SL extends PHP_Token {} class PHP_Token_SL_EQUAL extends PHP_Token {} class PHP_Token_SR extends PHP_Token {} class PHP_Token_SR_EQUAL extends PHP_Token {} class PHP_Token_START_HEREDOC extends PHP_Token {} class PHP_Token_STATIC extends PHP_Token {} class PHP_Token_STRING extends PHP_Token {} class PHP_Token_STRING_CAST extends PHP_Token {} class PHP_Token_STRING_VARNAME extends PHP_Token {} class PHP_Token_SWITCH extends PHP_Token {} class PHP_Token_THROW extends PHP_Token {} class PHP_Token_TILDE extends PHP_Token {} class PHP_Token_TRY extends PHP_Token {} class PHP_Token_UNSET extends PHP_Token {} class PHP_Token_UNSET_CAST extends PHP_Token {} class PHP_Token_USE extends PHP_Token {} class PHP_Token_USE_FUNCTION extends PHP_Token {} class PHP_Token_VAR extends PHP_Token {} class PHP_Token_VARIABLE extends PHP_Token {} class PHP_Token_WHILE extends PHP_Token {} class PHP_Token_WHITESPACE extends PHP_Token {} class PHP_Token_XOR_EQUAL extends PHP_Token {} // Tokens introduced in PHP 5.1 class PHP_Token_HALT_COMPILER extends PHP_Token {} // Tokens introduced in PHP 5.3 class PHP_Token_DIR extends PHP_Token {} class PHP_Token_GOTO extends PHP_Token {} class PHP_Token_NAMESPACE extends PHP_TokenWithScope { /** * @return string */ public function getName() { $tokens = $this->tokenStream->tokens(); $namespace = (string)$tokens[$this->id+2]; for ($i = $this->id + 3;; $i += 2) { if (isset($tokens[$i]) && $tokens[$i] instanceof PHP_Token_NS_SEPARATOR) { $namespace .= '\\' . $tokens[$i+1]; } else { break; } } return $namespace; } } class PHP_Token_NS_C extends PHP_Token {} class PHP_Token_NS_SEPARATOR extends PHP_Token {} // Tokens introduced in PHP 5.4 class PHP_Token_CALLABLE extends PHP_Token {} class PHP_Token_INSTEADOF extends PHP_Token {} class PHP_Token_TRAIT extends PHP_Token_INTERFACE {} class PHP_Token_TRAIT_C extends PHP_Token {} // Tokens introduced in PHP 5.5 class PHP_Token_FINALLY extends PHP_Token {} class PHP_Token_YIELD extends PHP_Token {} // Tokens introduced in PHP 5.6 class PHP_Token_ELLIPSIS extends PHP_Token {} class PHP_Token_POW extends PHP_Token {} class PHP_Token_POW_EQUAL extends PHP_Token {} // Tokens introduced in PHP 7.0 class PHP_Token_COALESCE extends PHP_Token {} class PHP_Token_SPACESHIP extends PHP_Token {} class PHP_Token_YIELD_FROM extends PHP_Token {} // Tokens introduced in HackLang / HHVM class PHP_Token_ASYNC extends PHP_Token {} class PHP_Token_AWAIT extends PHP_Token {} class PHP_Token_COMPILER_HALT_OFFSET extends PHP_Token {} class PHP_Token_ENUM extends PHP_Token {} class PHP_Token_EQUALS extends PHP_Token {} class PHP_Token_IN extends PHP_Token {} class PHP_Token_JOIN extends PHP_Token {} class PHP_Token_LAMBDA_ARROW extends PHP_Token {} class PHP_Token_LAMBDA_CP extends PHP_Token {} class PHP_Token_LAMBDA_OP extends PHP_Token {} class PHP_Token_ONUMBER extends PHP_Token {} class PHP_Token_SHAPE extends PHP_Token {} class PHP_Token_SUPER extends PHP_Token {} class PHP_Token_TYPE extends PHP_Token {} class PHP_Token_TYPELIST_GT extends PHP_Token {} class PHP_Token_TYPELIST_LT extends PHP_Token {} class PHP_Token_WHERE extends PHP_Token {} class PHP_Token_XHP_ATTRIBUTE extends PHP_Token {} class PHP_Token_XHP_CATEGORY extends PHP_Token {} class PHP_Token_XHP_CATEGORY_LABEL extends PHP_Token {} class PHP_Token_XHP_CHILDREN extends PHP_Token {} class PHP_Token_XHP_LABEL extends PHP_Token {} class PHP_Token_XHP_REQUIRED extends PHP_Token {} class PHP_Token_XHP_TAG_GT extends PHP_Token {} class PHP_Token_XHP_TAG_LT extends PHP_Token {} class PHP_Token_XHP_TEXT extends PHP_Token {} * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Marker interface for PHPUnit exceptions. * * @since Interface available since Release 4.0.0 */ interface PHPUnit_Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * We have a TestSuite object A. * In TestSuite object A we have Tests tagged with @group. * We want a TestSuite object B that contains TestSuite objects C, D, ... * for the Tests tagged with @group C, @group D, ... * Running the Tests from TestSuite object B results in Tests tagged with both * * @group C and @group D in TestSuite object A to be run twice . * * * $suite = new PHPUnit_Extensions_GroupTestSuite($A, array('C', 'D')); * * * @since Class available since Release 3.3.0 */ class PHPUnit_Extensions_GroupTestSuite extends PHPUnit_Framework_TestSuite { public function __construct(PHPUnit_Framework_TestSuite $suite, array $groups) { $groupSuites = array(); $name = $suite->getName(); foreach ($groups as $group) { $groupSuites[$group] = new PHPUnit_Framework_TestSuite($name . ' - ' . $group); $this->addTest($groupSuites[$group]); } $tests = new RecursiveIteratorIterator( new PHPUnit_Util_TestSuiteIterator($suite), RecursiveIteratorIterator::LEAVES_ONLY ); foreach ($tests as $test) { if ($test instanceof PHPUnit_Framework_TestCase) { $testGroups = PHPUnit_Util_Test::getGroups( get_class($test), $test->getName(false) ); foreach ($groups as $group) { foreach ($testGroups as $testGroup) { if ($group == $testGroup) { $groupSuites[$group]->addTest($test); } } } } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Runner for PHPT test cases. * * @since Class available since Release 3.1.4 */ class PHPUnit_Extensions_PhptTestCase implements PHPUnit_Framework_Test, PHPUnit_Framework_SelfDescribing { /** * @var string */ private $filename; /** * @var array */ private $settings = array( 'allow_url_fopen=1', 'auto_append_file=', 'auto_prepend_file=', 'disable_functions=', 'display_errors=1', 'docref_root=', 'docref_ext=.html', 'error_append_string=', 'error_prepend_string=', 'error_reporting=-1', 'html_errors=0', 'log_errors=0', 'magic_quotes_runtime=0', 'output_handler=', 'open_basedir=', 'output_buffering=Off', 'report_memleaks=0', 'report_zend_debug=0', 'safe_mode=0', 'track_errors=1', 'xdebug.default_enable=0' ); /** * Constructs a test case with the given filename. * * @param string $filename * * @throws PHPUnit_Framework_Exception */ public function __construct($filename) { if (!is_string($filename)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_file($filename)) { throw new PHPUnit_Framework_Exception( sprintf( 'File "%s" does not exist.', $filename ) ); } $this->filename = $filename; } /** * Counts the number of test cases executed by run(TestResult result). * * @return int */ public function count() { return 1; } /** * Runs a test and collects its result in a TestResult instance. * * @param PHPUnit_Framework_TestResult $result * * @return PHPUnit_Framework_TestResult */ public function run(PHPUnit_Framework_TestResult $result = null) { $sections = $this->parse(); $code = $this->render($sections['FILE']); if ($result === null) { $result = new PHPUnit_Framework_TestResult; } $php = PHPUnit_Util_PHP::factory(); $skip = false; $time = 0; $settings = $this->settings; $result->startTest($this); if (isset($sections['INI'])) { $settings = array_merge($settings, $this->parseIniSection($sections['INI'])); } if (isset($sections['SKIPIF'])) { $jobResult = $php->runJob($sections['SKIPIF'], $settings); if (!strncasecmp('skip', ltrim($jobResult['stdout']), 4)) { if (preg_match('/^\s*skip\s*(.+)\s*/i', $jobResult['stdout'], $message)) { $message = substr($message[1], 2); } else { $message = ''; } $result->addFailure($this, new PHPUnit_Framework_SkippedTestError($message), 0); $skip = true; } } if (!$skip) { PHP_Timer::start(); $jobResult = $php->runJob($code, $settings); $time = PHP_Timer::stop(); if (isset($sections['EXPECT'])) { $assertion = 'assertEquals'; $expected = $sections['EXPECT']; } else { $assertion = 'assertStringMatchesFormat'; $expected = $sections['EXPECTF']; } $output = preg_replace('/\r\n/', "\n", trim($jobResult['stdout'])); $expected = preg_replace('/\r\n/', "\n", trim($expected)); try { PHPUnit_Framework_Assert::$assertion($expected, $output); } catch (PHPUnit_Framework_AssertionFailedError $e) { $result->addFailure($this, $e, $time); } catch (Throwable $t) { $result->addError($this, $t, $time); } catch (Exception $e) { $result->addError($this, $e, $time); } } $result->endTest($this, $time); return $result; } /** * Returns the name of the test case. * * @return string */ public function getName() { return $this->toString(); } /** * Returns a string representation of the test case. * * @return string */ public function toString() { return $this->filename; } /** * @return array * * @throws PHPUnit_Framework_Exception */ private function parse() { $sections = array(); $section = ''; foreach (file($this->filename) as $line) { if (preg_match('/^--([_A-Z]+)--/', $line, $result)) { $section = $result[1]; $sections[$section] = ''; continue; } elseif (empty($section)) { throw new PHPUnit_Framework_Exception('Invalid PHPT file'); } $sections[$section] .= $line; } if (!isset($sections['FILE']) || (!isset($sections['EXPECT']) && !isset($sections['EXPECTF']))) { throw new PHPUnit_Framework_Exception('Invalid PHPT file'); } return $sections; } /** * @param string $code * * @return string */ private function render($code) { return str_replace( array( '__DIR__', '__FILE__' ), array( "'" . dirname($this->filename) . "'", "'" . $this->filename . "'" ), $code ); } /** * Parse --INI-- section key value pairs and return as array. * * @param string * * @return array */ protected function parseIniSection($content) { return preg_split('/\n|\r/', $content, -1, PREG_SPLIT_NO_EMPTY); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Suite for .phpt test cases. * * @since Class available since Release 3.1.4 */ class PHPUnit_Extensions_PhptTestSuite extends PHPUnit_Framework_TestSuite { /** * Constructs a new TestSuite for .phpt test cases. * * @param string $directory * * @throws PHPUnit_Framework_Exception */ public function __construct($directory) { if (is_string($directory) && is_dir($directory)) { $this->setName($directory); $facade = new File_Iterator_Facade; $files = $facade->getFilesAsArray($directory, '.phpt'); foreach ($files as $file) { $this->addTestFile($file); } } else { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'directory name'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A Decorator that runs a test repeatedly. * * @since Class available since Release 2.0.0 */ class PHPUnit_Extensions_RepeatedTest extends PHPUnit_Extensions_TestDecorator { /** * @var bool */ protected $processIsolation = false; /** * @var int */ protected $timesRepeat = 1; /** * @param PHPUnit_Framework_Test $test * @param int $timesRepeat * @param bool $processIsolation * * @throws PHPUnit_Framework_Exception */ public function __construct(PHPUnit_Framework_Test $test, $timesRepeat = 1, $processIsolation = false) { parent::__construct($test); if (is_integer($timesRepeat) && $timesRepeat >= 0) { $this->timesRepeat = $timesRepeat; } else { throw PHPUnit_Util_InvalidArgumentHelper::factory( 2, 'positive integer' ); } $this->processIsolation = $processIsolation; } /** * Counts the number of test cases that * will be run by this test. * * @return int */ public function count() { return $this->timesRepeat * count($this->test); } /** * Runs the decorated test and collects the * result in a TestResult. * * @param PHPUnit_Framework_TestResult $result * * @return PHPUnit_Framework_TestResult * * @throws PHPUnit_Framework_Exception */ public function run(PHPUnit_Framework_TestResult $result = null) { if ($result === null) { $result = $this->createResult(); } //@codingStandardsIgnoreStart for ($i = 0; $i < $this->timesRepeat && !$result->shouldStop(); $i++) { //@codingStandardsIgnoreEnd if ($this->test instanceof PHPUnit_Framework_TestSuite) { $this->test->setRunTestInSeparateProcess($this->processIsolation); } $this->test->run($result); } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A Decorator for Tests. * * Use TestDecorator as the base class for defining new * test decorators. Test decorator subclasses can be introduced * to add behaviour before or after a test is run. * * @since Class available since Release 2.0.0 */ class PHPUnit_Extensions_TestDecorator extends PHPUnit_Framework_Assert implements PHPUnit_Framework_Test, PHPUnit_Framework_SelfDescribing { /** * The Test to be decorated. * * @var object */ protected $test = null; /** * Constructor. * * @param PHPUnit_Framework_Test $test */ public function __construct(PHPUnit_Framework_Test $test) { $this->test = $test; } /** * Returns a string representation of the test. * * @return string */ public function toString() { return $this->test->toString(); } /** * Runs the test and collects the * result in a TestResult. * * @param PHPUnit_Framework_TestResult $result */ public function basicRun(PHPUnit_Framework_TestResult $result) { $this->test->run($result); } /** * Counts the number of test cases that * will be run by this test. * * @return int */ public function count() { return count($this->test); } /** * Creates a default TestResult object. * * @return PHPUnit_Framework_TestResult */ protected function createResult() { return new PHPUnit_Framework_TestResult; } /** * Returns the test to be run. * * @return PHPUnit_Framework_Test */ public function getTest() { return $this->test; } /** * Runs the decorated test and collects the * result in a TestResult. * * @param PHPUnit_Framework_TestResult $result * * @return PHPUnit_Framework_TestResult */ public function run(PHPUnit_Framework_TestResult $result = null) { if ($result === null) { $result = $this->createResult(); } $this->basicRun($result); return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Base class for test listeners that interact with an issue tracker. * * @since Class available since Release 3.4.0 */ abstract class PHPUnit_Extensions_TicketListener implements PHPUnit_Framework_TestListener { /** * @var array */ protected $ticketCounts = array(); /** * @var bool */ protected $ran = false; /** * An error occurred. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { } /** * A failure occurred. * * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_AssertionFailedError $e * @param float $time */ public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { } /** * Incomplete test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { } /** * Risky test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 4.0.0 */ public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { } /** * Skipped test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 3.0.0 */ public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { } /** * A test suite started. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function startTestSuite(PHPUnit_Framework_TestSuite $suite) { } /** * A test suite ended. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function endTestSuite(PHPUnit_Framework_TestSuite $suite) { } /** * A test started. * * @param PHPUnit_Framework_Test $test */ public function startTest(PHPUnit_Framework_Test $test) { if (!$test instanceof PHPUnit_Framework_Warning) { if ($this->ran) { return; } $name = $test->getName(false); $tickets = PHPUnit_Util_Test::getTickets(get_class($test), $name); foreach ($tickets as $ticket) { $this->ticketCounts[$ticket][$name] = 1; } $this->ran = true; } } /** * A test ended. * * @param PHPUnit_Framework_Test $test * @param float $time */ public function endTest(PHPUnit_Framework_Test $test, $time) { if (!$test instanceof PHPUnit_Framework_Warning) { if ($test->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED) { $ifStatus = array('assigned', 'new', 'reopened'); $newStatus = 'closed'; $message = 'Automatically closed by PHPUnit (test passed).'; $resolution = 'fixed'; $cumulative = true; } elseif ($test->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE) { $ifStatus = array('closed'); $newStatus = 'reopened'; $message = 'Automatically reopened by PHPUnit (test failed).'; $resolution = ''; $cumulative = false; } else { return; } $name = $test->getName(false); $tickets = PHPUnit_Util_Test::getTickets(get_class($test), $name); foreach ($tickets as $ticket) { // Remove this test from the totals (if it passed). if ($test->getStatus() == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED) { unset($this->ticketCounts[$ticket][$name]); } // Only close tickets if ALL referenced cases pass // but reopen tickets if a single test fails. if ($cumulative) { // Determine number of to-pass tests: if (count($this->ticketCounts[$ticket]) > 0) { // There exist remaining test cases with this reference. $adjustTicket = false; } else { // No remaining tickets, go ahead and adjust. $adjustTicket = true; } } else { $adjustTicket = true; } $ticketInfo = $this->getTicketInfo($ticket); if ($adjustTicket && in_array($ticketInfo['status'], $ifStatus)) { $this->updateTicket($ticket, $newStatus, $message, $resolution); } } } } /** * @param mixed $ticketId * * @return mixed */ abstract protected function getTicketInfo($ticketId = null); /** * @param string $ticketId * @param string $newStatus * @param string $message * @param string $resolution */ abstract protected function updateTicket($ticketId, $newStatus, $message, $resolution); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Returns a matcher that matches when the method is executed * zero or more times. * * @return PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount * * @since Method available since Release 3.0.0 */ function any() { return call_user_func_array( 'PHPUnit_Framework_TestCase::any', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsAnything matcher object. * * @return PHPUnit_Framework_Constraint_IsAnything * * @since Method available since Release 3.0.0 */ function anything() { return call_user_func_array( 'PHPUnit_Framework_Assert::anything', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_ArrayHasKey matcher object. * * @param mixed $key * * @return PHPUnit_Framework_Constraint_ArrayHasKey * * @since Method available since Release 3.0.0 */ function arrayHasKey($key) { return call_user_func_array( 'PHPUnit_Framework_Assert::arrayHasKey', func_get_args() ); } /** * Asserts that an array has a specified key. * * @param mixed $key * @param array|ArrayAccess $array * @param string $message * * @since Method available since Release 3.0.0 */ function assertArrayHasKey($key, $array, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertArrayHasKey', func_get_args() ); } /** * Asserts that an array has a specified subset. * * @param array|ArrayAccess $subset * @param array|ArrayAccess $array * @param bool $strict Check for object identity * @param string $message * * @since Method available since Release 4.4.0 */ function assertArraySubset($subset, $array, $strict = false, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertArraySubset', func_get_args() ); } /** * Asserts that an array does not have a specified key. * * @param mixed $key * @param array|ArrayAccess $array * @param string $message * * @since Method available since Release 3.0.0 */ function assertArrayNotHasKey($key, $array, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertArrayNotHasKey', func_get_args() ); } /** * Asserts that a haystack that is stored in a static attribute of a class * or an attribute of an object contains a needle. * * @param mixed $needle * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * @param bool $ignoreCase * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @since Method available since Release 3.0.0 */ function assertAttributeContains($needle, $haystackAttributeName, $haystackClassOrObject, $message = '', $ignoreCase = false, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeContains', func_get_args() ); } /** * Asserts that a haystack that is stored in a static attribute of a class * or an attribute of an object contains only values of a given type. * * @param string $type * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param bool $isNativeType * @param string $message * * @since Method available since Release 3.1.4 */ function assertAttributeContainsOnly($type, $haystackAttributeName, $haystackClassOrObject, $isNativeType = null, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeContainsOnly', func_get_args() ); } /** * Asserts the number of elements of an array, Countable or Traversable * that is stored in an attribute. * * @param int $expectedCount * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * * @since Method available since Release 3.6.0 */ function assertAttributeCount($expectedCount, $haystackAttributeName, $haystackClassOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeCount', func_get_args() ); } /** * Asserts that a static attribute of a class or an attribute of an object * is empty. * * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * * @since Method available since Release 3.5.0 */ function assertAttributeEmpty($haystackAttributeName, $haystackClassOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeEmpty', func_get_args() ); } /** * Asserts that a variable is equal to an attribute of an object. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase */ function assertAttributeEquals($expected, $actualAttributeName, $actualClassOrObject, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeEquals', func_get_args() ); } /** * Asserts that an attribute is greater than another value. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * * @since Method available since Release 3.1.0 */ function assertAttributeGreaterThan($expected, $actualAttributeName, $actualClassOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeGreaterThan', func_get_args() ); } /** * Asserts that an attribute is greater than or equal to another value. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * * @since Method available since Release 3.1.0 */ function assertAttributeGreaterThanOrEqual($expected, $actualAttributeName, $actualClassOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeGreaterThanOrEqual', func_get_args() ); } /** * Asserts that an attribute is of a given type. * * @param string $expected * @param string $attributeName * @param mixed $classOrObject * @param string $message * * @since Method available since Release 3.5.0 */ function assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeInstanceOf', func_get_args() ); } /** * Asserts that an attribute is of a given type. * * @param string $expected * @param string $attributeName * @param mixed $classOrObject * @param string $message * * @since Method available since Release 3.5.0 */ function assertAttributeInternalType($expected, $attributeName, $classOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeInternalType', func_get_args() ); } /** * Asserts that an attribute is smaller than another value. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * * @since Method available since Release 3.1.0 */ function assertAttributeLessThan($expected, $actualAttributeName, $actualClassOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeLessThan', func_get_args() ); } /** * Asserts that an attribute is smaller than or equal to another value. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * * @since Method available since Release 3.1.0 */ function assertAttributeLessThanOrEqual($expected, $actualAttributeName, $actualClassOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeLessThanOrEqual', func_get_args() ); } /** * Asserts that a haystack that is stored in a static attribute of a class * or an attribute of an object does not contain a needle. * * @param mixed $needle * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * @param bool $ignoreCase * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @since Method available since Release 3.0.0 */ function assertAttributeNotContains($needle, $haystackAttributeName, $haystackClassOrObject, $message = '', $ignoreCase = false, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeNotContains', func_get_args() ); } /** * Asserts that a haystack that is stored in a static attribute of a class * or an attribute of an object does not contain only values of a given * type. * * @param string $type * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param bool $isNativeType * @param string $message * * @since Method available since Release 3.1.4 */ function assertAttributeNotContainsOnly($type, $haystackAttributeName, $haystackClassOrObject, $isNativeType = null, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeNotContainsOnly', func_get_args() ); } /** * Asserts the number of elements of an array, Countable or Traversable * that is stored in an attribute. * * @param int $expectedCount * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * * @since Method available since Release 3.6.0 */ function assertAttributeNotCount($expectedCount, $haystackAttributeName, $haystackClassOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeNotCount', func_get_args() ); } /** * Asserts that a static attribute of a class or an attribute of an object * is not empty. * * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * * @since Method available since Release 3.5.0 */ function assertAttributeNotEmpty($haystackAttributeName, $haystackClassOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeNotEmpty', func_get_args() ); } /** * Asserts that a variable is not equal to an attribute of an object. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase */ function assertAttributeNotEquals($expected, $actualAttributeName, $actualClassOrObject, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeNotEquals', func_get_args() ); } /** * Asserts that an attribute is of a given type. * * @param string $expected * @param string $attributeName * @param mixed $classOrObject * @param string $message * * @since Method available since Release 3.5.0 */ function assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeNotInstanceOf', func_get_args() ); } /** * Asserts that an attribute is of a given type. * * @param string $expected * @param string $attributeName * @param mixed $classOrObject * @param string $message * * @since Method available since Release 3.5.0 */ function assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeNotInternalType', func_get_args() ); } /** * Asserts that a variable and an attribute of an object do not have the * same type and value. * * @param mixed $expected * @param string $actualAttributeName * @param object $actualClassOrObject * @param string $message */ function assertAttributeNotSame($expected, $actualAttributeName, $actualClassOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeNotSame', func_get_args() ); } /** * Asserts that a variable and an attribute of an object have the same type * and value. * * @param mixed $expected * @param string $actualAttributeName * @param object $actualClassOrObject * @param string $message */ function assertAttributeSame($expected, $actualAttributeName, $actualClassOrObject, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertAttributeSame', func_get_args() ); } /** * Asserts that a class has a specified attribute. * * @param string $attributeName * @param string $className * @param string $message * * @since Method available since Release 3.1.0 */ function assertClassHasAttribute($attributeName, $className, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertClassHasAttribute', func_get_args() ); } /** * Asserts that a class has a specified static attribute. * * @param string $attributeName * @param string $className * @param string $message * * @since Method available since Release 3.1.0 */ function assertClassHasStaticAttribute($attributeName, $className, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertClassHasStaticAttribute', func_get_args() ); } /** * Asserts that a class does not have a specified attribute. * * @param string $attributeName * @param string $className * @param string $message * * @since Method available since Release 3.1.0 */ function assertClassNotHasAttribute($attributeName, $className, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertClassNotHasAttribute', func_get_args() ); } /** * Asserts that a class does not have a specified static attribute. * * @param string $attributeName * @param string $className * @param string $message * * @since Method available since Release 3.1.0 */ function assertClassNotHasStaticAttribute($attributeName, $className, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertClassNotHasStaticAttribute', func_get_args() ); } /** * Asserts that a haystack contains a needle. * * @param mixed $needle * @param mixed $haystack * @param string $message * @param bool $ignoreCase * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @since Method available since Release 2.1.0 */ function assertContains($needle, $haystack, $message = '', $ignoreCase = false, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertContains', func_get_args() ); } /** * Asserts that a haystack contains only values of a given type. * * @param string $type * @param mixed $haystack * @param bool $isNativeType * @param string $message * * @since Method available since Release 3.1.4 */ function assertContainsOnly($type, $haystack, $isNativeType = null, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertContainsOnly', func_get_args() ); } /** * Asserts that a haystack contains only instances of a given classname * * @param string $classname * @param array|Traversable $haystack * @param string $message */ function assertContainsOnlyInstancesOf($classname, $haystack, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertContainsOnlyInstancesOf', func_get_args() ); } /** * Asserts the number of elements of an array, Countable or Traversable. * * @param int $expectedCount * @param mixed $haystack * @param string $message */ function assertCount($expectedCount, $haystack, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertCount', func_get_args() ); } /** * Asserts that a variable is empty. * * @param mixed $actual * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ function assertEmpty($actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertEmpty', func_get_args() ); } /** * Asserts that a hierarchy of DOMElements matches. * * @param DOMElement $expectedElement * @param DOMElement $actualElement * @param bool $checkAttributes * @param string $message * * @since Method available since Release 3.3.0 */ function assertEqualXMLStructure(DOMElement $expectedElement, DOMElement $actualElement, $checkAttributes = false, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertEqualXMLStructure', func_get_args() ); } /** * Asserts that two variables are equal. * * @param mixed $expected * @param mixed $actual * @param string $message * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase */ function assertEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertEquals', func_get_args() ); } /** * Asserts that a condition is not true. * * @param bool $condition * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ function assertNotTrue($condition, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotTrue', func_get_args() ); } /** * Asserts that a condition is false. * * @param bool $condition * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ function assertFalse($condition, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertFalse', func_get_args() ); } /** * Asserts that the contents of one file is equal to the contents of another * file. * * @param string $expected * @param string $actual * @param string $message * @param bool $canonicalize * @param bool $ignoreCase * * @since Method available since Release 3.2.14 */ function assertFileEquals($expected, $actual, $message = '', $canonicalize = false, $ignoreCase = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertFileEquals', func_get_args() ); } /** * Asserts that a file exists. * * @param string $filename * @param string $message * * @since Method available since Release 3.0.0 */ function assertFileExists($filename, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertFileExists', func_get_args() ); } /** * Asserts that the contents of one file is not equal to the contents of * another file. * * @param string $expected * @param string $actual * @param string $message * @param bool $canonicalize * @param bool $ignoreCase * * @since Method available since Release 3.2.14 */ function assertFileNotEquals($expected, $actual, $message = '', $canonicalize = false, $ignoreCase = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertFileNotEquals', func_get_args() ); } /** * Asserts that a file does not exist. * * @param string $filename * @param string $message * * @since Method available since Release 3.0.0 */ function assertFileNotExists($filename, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertFileNotExists', func_get_args() ); } /** * Asserts that a value is greater than another value. * * @param mixed $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.1.0 */ function assertGreaterThan($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertGreaterThan', func_get_args() ); } /** * Asserts that a value is greater than or equal to another value. * * @param mixed $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.1.0 */ function assertGreaterThanOrEqual($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertGreaterThanOrEqual', func_get_args() ); } /** * Asserts that a variable is of a given type. * * @param string $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.5.0 */ function assertInstanceOf($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertInstanceOf', func_get_args() ); } /** * Asserts that a variable is of a given type. * * @param string $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.5.0 */ function assertInternalType($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertInternalType', func_get_args() ); } /** * Asserts that a string is a valid JSON string. * * @param string $actualJson * @param string $message * * @since Method available since Release 3.7.20 */ function assertJson($actualJson, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertJson', func_get_args() ); } /** * Asserts that two JSON files are equal. * * @param string $expectedFile * @param string $actualFile * @param string $message */ function assertJsonFileEqualsJsonFile($expectedFile, $actualFile, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertJsonFileEqualsJsonFile', func_get_args() ); } /** * Asserts that two JSON files are not equal. * * @param string $expectedFile * @param string $actualFile * @param string $message */ function assertJsonFileNotEqualsJsonFile($expectedFile, $actualFile, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertJsonFileNotEqualsJsonFile', func_get_args() ); } /** * Asserts that the generated JSON encoded object and the content of the given file are equal. * * @param string $expectedFile * @param string $actualJson * @param string $message */ function assertJsonStringEqualsJsonFile($expectedFile, $actualJson, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertJsonStringEqualsJsonFile', func_get_args() ); } /** * Asserts that two given JSON encoded objects or arrays are equal. * * @param string $expectedJson * @param string $actualJson * @param string $message */ function assertJsonStringEqualsJsonString($expectedJson, $actualJson, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertJsonStringEqualsJsonString', func_get_args() ); } /** * Asserts that the generated JSON encoded object and the content of the given file are not equal. * * @param string $expectedFile * @param string $actualJson * @param string $message */ function assertJsonStringNotEqualsJsonFile($expectedFile, $actualJson, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertJsonStringNotEqualsJsonFile', func_get_args() ); } /** * Asserts that two given JSON encoded objects or arrays are not equal. * * @param string $expectedJson * @param string $actualJson * @param string $message */ function assertJsonStringNotEqualsJsonString($expectedJson, $actualJson, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertJsonStringNotEqualsJsonString', func_get_args() ); } /** * Asserts that a value is smaller than another value. * * @param mixed $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.1.0 */ function assertLessThan($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertLessThan', func_get_args() ); } /** * Asserts that a value is smaller than or equal to another value. * * @param mixed $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.1.0 */ function assertLessThanOrEqual($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertLessThanOrEqual', func_get_args() ); } /** * Asserts that a haystack does not contain a needle. * * @param mixed $needle * @param mixed $haystack * @param string $message * @param bool $ignoreCase * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @since Method available since Release 2.1.0 */ function assertNotContains($needle, $haystack, $message = '', $ignoreCase = false, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotContains', func_get_args() ); } /** * Asserts that a haystack does not contain only values of a given type. * * @param string $type * @param mixed $haystack * @param bool $isNativeType * @param string $message * * @since Method available since Release 3.1.4 */ function assertNotContainsOnly($type, $haystack, $isNativeType = null, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotContainsOnly', func_get_args() ); } /** * Asserts the number of elements of an array, Countable or Traversable. * * @param int $expectedCount * @param mixed $haystack * @param string $message */ function assertNotCount($expectedCount, $haystack, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotCount', func_get_args() ); } /** * Asserts that a variable is not empty. * * @param mixed $actual * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ function assertNotEmpty($actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotEmpty', func_get_args() ); } /** * Asserts that two variables are not equal. * * @param mixed $expected * @param mixed $actual * @param string $message * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase * * @since Method available since Release 2.3.0 */ function assertNotEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotEquals', func_get_args() ); } /** * Asserts that a variable is not of a given type. * * @param string $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.5.0 */ function assertNotInstanceOf($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotInstanceOf', func_get_args() ); } /** * Asserts that a variable is not of a given type. * * @param string $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.5.0 */ function assertNotInternalType($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotInternalType', func_get_args() ); } /** * Asserts that a condition is not false. * * @param bool $condition * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ function assertNotFalse($condition, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotFalse', func_get_args() ); } /** * Asserts that a variable is not null. * * @param mixed $actual * @param string $message */ function assertNotNull($actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotNull', func_get_args() ); } /** * Asserts that a string does not match a given regular expression. * * @param string $pattern * @param string $string * @param string $message * * @since Method available since Release 2.1.0 */ function assertNotRegExp($pattern, $string, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotRegExp', func_get_args() ); } /** * Asserts that two variables do not have the same type and value. * Used on objects, it asserts that two variables do not reference * the same object. * * @param mixed $expected * @param mixed $actual * @param string $message */ function assertNotSame($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotSame', func_get_args() ); } /** * Assert that the size of two arrays (or `Countable` or `Traversable` objects) * is not the same. * * @param array|Countable|Traversable $expected * @param array|Countable|Traversable $actual * @param string $message */ function assertNotSameSize($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotSameSize', func_get_args() ); } /** * This assertion is the exact opposite of assertTag(). * * Rather than asserting that $matcher results in a match, it asserts that * $matcher does not match. * * @param array $matcher * @param string $actual * @param string $message * @param bool $isHtml * * @since Method available since Release 3.3.0 */ function assertNotTag($matcher, $actual, $message = '', $isHtml = true) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNotTag', func_get_args() ); } /** * Asserts that a variable is null. * * @param mixed $actual * @param string $message */ function assertNull($actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertNull', func_get_args() ); } /** * Asserts that an object has a specified attribute. * * @param string $attributeName * @param object $object * @param string $message * * @since Method available since Release 3.0.0 */ function assertObjectHasAttribute($attributeName, $object, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertObjectHasAttribute', func_get_args() ); } /** * Asserts that an object does not have a specified attribute. * * @param string $attributeName * @param object $object * @param string $message * * @since Method available since Release 3.0.0 */ function assertObjectNotHasAttribute($attributeName, $object, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertObjectNotHasAttribute', func_get_args() ); } /** * Asserts that a string matches a given regular expression. * * @param string $pattern * @param string $string * @param string $message */ function assertRegExp($pattern, $string, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertRegExp', func_get_args() ); } /** * Asserts that two variables have the same type and value. * Used on objects, it asserts that two variables reference * the same object. * * @param mixed $expected * @param mixed $actual * @param string $message */ function assertSame($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertSame', func_get_args() ); } /** * Assert that the size of two arrays (or `Countable` or `Traversable` objects) * is the same. * * @param array|Countable|Traversable $expected * @param array|Countable|Traversable $actual * @param string $message */ function assertSameSize($expected, $actual, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertSameSize', func_get_args() ); } /** * Assert the presence, absence, or count of elements in a document matching * the CSS $selector, regardless of the contents of those elements. * * The first argument, $selector, is the CSS selector used to match * the elements in the $actual document. * * The second argument, $count, can be either boolean or numeric. * When boolean, it asserts for presence of elements matching the selector * (true) or absence of elements (false). * When numeric, it asserts the count of elements. * * assertSelectCount("#binder", true, $xml); // any? * assertSelectCount(".binder", 3, $xml); // exactly 3? * * @param array $selector * @param int $count * @param mixed $actual * @param string $message * @param bool $isHtml * * @since Method available since Release 3.3.0 */ function assertSelectCount($selector, $count, $actual, $message = '', $isHtml = true) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertSelectCount', func_get_args() ); } /** * assertSelectEquals("#binder .name", "Chuck", true, $xml); // any? * assertSelectEquals("#binder .name", "Chuck", false, $xml); // none? * * @param array $selector * @param string $content * @param int $count * @param mixed $actual * @param string $message * @param bool $isHtml * * @since Method available since Release 3.3.0 */ function assertSelectEquals($selector, $content, $count, $actual, $message = '', $isHtml = true) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertSelectEquals', func_get_args() ); } /** * assertSelectRegExp("#binder .name", "/Mike|Derek/", true, $xml); // any? * assertSelectRegExp("#binder .name", "/Mike|Derek/", 3, $xml);// 3? * * @param array $selector * @param string $pattern * @param int $count * @param mixed $actual * @param string $message * @param bool $isHtml * * @since Method available since Release 3.3.0 */ function assertSelectRegExp($selector, $pattern, $count, $actual, $message = '', $isHtml = true) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertSelectRegExp', func_get_args() ); } /** * Asserts that a string ends not with a given prefix. * * @param string $suffix * @param string $string * @param string $message * * @since Method available since Release 3.4.0 */ function assertStringEndsNotWith($suffix, $string, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertStringEndsNotWith', func_get_args() ); } /** * Asserts that a string ends with a given prefix. * * @param string $suffix * @param string $string * @param string $message * * @since Method available since Release 3.4.0 */ function assertStringEndsWith($suffix, $string, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertStringEndsWith', func_get_args() ); } /** * Asserts that the contents of a string is equal * to the contents of a file. * * @param string $expectedFile * @param string $actualString * @param string $message * @param bool $canonicalize * @param bool $ignoreCase * * @since Method available since Release 3.3.0 */ function assertStringEqualsFile($expectedFile, $actualString, $message = '', $canonicalize = false, $ignoreCase = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertStringEqualsFile', func_get_args() ); } /** * Asserts that a string matches a given format string. * * @param string $format * @param string $string * @param string $message * * @since Method available since Release 3.5.0 */ function assertStringMatchesFormat($format, $string, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertStringMatchesFormat', func_get_args() ); } /** * Asserts that a string matches a given format file. * * @param string $formatFile * @param string $string * @param string $message * * @since Method available since Release 3.5.0 */ function assertStringMatchesFormatFile($formatFile, $string, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertStringMatchesFormatFile', func_get_args() ); } /** * Asserts that the contents of a string is not equal * to the contents of a file. * * @param string $expectedFile * @param string $actualString * @param string $message * @param bool $canonicalize * @param bool $ignoreCase * * @since Method available since Release 3.3.0 */ function assertStringNotEqualsFile($expectedFile, $actualString, $message = '', $canonicalize = false, $ignoreCase = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertStringNotEqualsFile', func_get_args() ); } /** * Asserts that a string does not match a given format string. * * @param string $format * @param string $string * @param string $message * * @since Method available since Release 3.5.0 */ function assertStringNotMatchesFormat($format, $string, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertStringNotMatchesFormat', func_get_args() ); } /** * Asserts that a string does not match a given format string. * * @param string $formatFile * @param string $string * @param string $message * * @since Method available since Release 3.5.0 */ function assertStringNotMatchesFormatFile($formatFile, $string, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertStringNotMatchesFormatFile', func_get_args() ); } /** * Asserts that a string starts not with a given prefix. * * @param string $prefix * @param string $string * @param string $message * * @since Method available since Release 3.4.0 */ function assertStringStartsNotWith($prefix, $string, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertStringStartsNotWith', func_get_args() ); } /** * Asserts that a string starts with a given prefix. * * @param string $prefix * @param string $string * @param string $message * * @since Method available since Release 3.4.0 */ function assertStringStartsWith($prefix, $string, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertStringStartsWith', func_get_args() ); } /** * Evaluate an HTML or XML string and assert its structure and/or contents. * * The first argument ($matcher) is an associative array that specifies the * match criteria for the assertion: * * - `id` : the node with the given id attribute must match the * corresponding value. * - `tag` : the node type must match the corresponding value. * - `attributes` : a hash. The node's attributes must match the * corresponding values in the hash. * - `content` : The text content must match the given value. * - `parent` : a hash. The node's parent must match the * corresponding hash. * - `child`: a hash. At least one of the node's immediate children * must meet the criteria described by the hash. * - `ancestor` : a hash. At least one of the node's ancestors must * meet the criteria described by the hash. * - `descendant` : a hash. At least one of the node's descendants must * meet the criteria described by the hash. * - `children` : a hash, for counting children of a node. * Accepts the keys: *- `count`: a number which must equal the number of children * that match *- `less_than`: the number of matching children must be greater * than this number *- `greater_than` : the number of matching children must be less than * this number *- `only` : another hash consisting of the keys to use to match * on the children, and only matching children will be * counted * * * // Matcher that asserts that there is an element with an id="my_id". * $matcher = array('id' => 'my_id'); * * // Matcher that asserts that there is a "span" tag. * $matcher = array('tag' => 'span'); * * // Matcher that asserts that there is a "span" tag with the content * // "Hello World". * $matcher = array('tag' => 'span', 'content' => 'Hello World'); * * // Matcher that asserts that there is a "span" tag with content matching * // the regular expression pattern. * $matcher = array('tag' => 'span', 'content' => 'regexp:/Try P(HP|ython)/'); * * // Matcher that asserts that there is a "span" with an "list" class * // attribute. * $matcher = array( * 'tag'=> 'span', * 'attributes' => array('class' => 'list') * ); * * // Matcher that asserts that there is a "span" inside of a "div". * $matcher = array( * 'tag'=> 'span', * 'parent' => array('tag' => 'div') * ); * * // Matcher that asserts that there is a "span" somewhere inside a * // "table". * $matcher = array( * 'tag' => 'span', * 'ancestor' => array('tag' => 'table') * ); * * // Matcher that asserts that there is a "span" with at least one "em" * // child. * $matcher = array( * 'tag' => 'span', * 'child' => array('tag' => 'em') * ); * * // Matcher that asserts that there is a "span" containing a (possibly * // nested) "strong" tag. * $matcher = array( * 'tag'=> 'span', * 'descendant' => array('tag' => 'strong') * ); * * // Matcher that asserts that there is a "span" containing 5-10 "em" tags * // as immediate children. * $matcher = array( * 'tag' => 'span', * 'children' => array( * 'less_than'=> 11, * 'greater_than' => 4, * 'only' => array('tag' => 'em') * ) * ); * * // Matcher that asserts that there is a "div", with an "ul" ancestor and * // a "li" parent (with class="enum"), and containing a "span" descendant * // that contains an element with id="my_test" and the text "Hello World". * $matcher = array( * 'tag'=> 'div', * 'ancestor' => array('tag' => 'ul'), * 'parent' => array( * 'tag'=> 'li', * 'attributes' => array('class' => 'enum') * ), * 'descendant' => array( * 'tag' => 'span', * 'child' => array( * 'id' => 'my_test', * 'content' => 'Hello World' * ) * ) * ); * * // Use assertTag() to apply a $matcher to a piece of $html. * $this->assertTag($matcher, $html); * * // Use assertTag() to apply a $matcher to a piece of $xml. * $this->assertTag($matcher, $xml, '', false); * * * The second argument ($actual) is a string containing either HTML or * XML text to be tested. * * The third argument ($message) is an optional message that will be * used if the assertion fails. * * The fourth argument ($html) is an optional flag specifying whether * to load the $actual string into a DOMDocument using the HTML or * XML load strategy. It is true by default, which assumes the HTML * load strategy. In many cases, this will be acceptable for XML as well. * * @param array $matcher * @param string $actual * @param string $message * @param bool $isHtml * * @since Method available since Release 3.3.0 */ function assertTag($matcher, $actual, $message = '', $isHtml = true) { return call_user_func_array( 'PHPUnit_Framework_Assert::assertTag', func_get_args() ); } /** * Evaluates a PHPUnit_Framework_Constraint matcher object. * * @param mixed $value * @param PHPUnit_Framework_Constraint $constraint * @param string $message * * @since Method available since Release 3.0.0 */ function assertThat($value, PHPUnit_Framework_Constraint $constraint, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertThat', func_get_args() ); } /** * Asserts that a condition is true. * * @param bool $condition * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ function assertTrue($condition, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertTrue', func_get_args() ); } /** * Asserts that two XML files are equal. * * @param string $expectedFile * @param string $actualFile * @param string $message * * @since Method available since Release 3.1.0 */ function assertXmlFileEqualsXmlFile($expectedFile, $actualFile, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertXmlFileEqualsXmlFile', func_get_args() ); } /** * Asserts that two XML files are not equal. * * @param string $expectedFile * @param string $actualFile * @param string $message * * @since Method available since Release 3.1.0 */ function assertXmlFileNotEqualsXmlFile($expectedFile, $actualFile, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertXmlFileNotEqualsXmlFile', func_get_args() ); } /** * Asserts that two XML documents are equal. * * @param string $expectedFile * @param string $actualXml * @param string $message * * @since Method available since Release 3.3.0 */ function assertXmlStringEqualsXmlFile($expectedFile, $actualXml, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertXmlStringEqualsXmlFile', func_get_args() ); } /** * Asserts that two XML documents are equal. * * @param string $expectedXml * @param string $actualXml * @param string $message * * @since Method available since Release 3.1.0 */ function assertXmlStringEqualsXmlString($expectedXml, $actualXml, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertXmlStringEqualsXmlString', func_get_args() ); } /** * Asserts that two XML documents are not equal. * * @param string $expectedFile * @param string $actualXml * @param string $message * * @since Method available since Release 3.3.0 */ function assertXmlStringNotEqualsXmlFile($expectedFile, $actualXml, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertXmlStringNotEqualsXmlFile', func_get_args() ); } /** * Asserts that two XML documents are not equal. * * @param string $expectedXml * @param string $actualXml * @param string $message * * @since Method available since Release 3.1.0 */ function assertXmlStringNotEqualsXmlString($expectedXml, $actualXml, $message = '') { return call_user_func_array( 'PHPUnit_Framework_Assert::assertXmlStringNotEqualsXmlString', func_get_args() ); } /** * Returns a matcher that matches when the method is executed * at the given $index. * * @param int $index * * @return PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex * * @since Method available since Release 3.0.0 */ function at($index) { return call_user_func_array( 'PHPUnit_Framework_TestCase::at', func_get_args() ); } /** * Returns a matcher that matches when the method is executed at least once. * * @return PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce * * @since Method available since Release 3.0.0 */ function atLeastOnce() { return call_user_func_array( 'PHPUnit_Framework_TestCase::atLeastOnce', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_Attribute matcher object. * * @param PHPUnit_Framework_Constraint $constraint * @param string $attributeName * * @return PHPUnit_Framework_Constraint_Attribute * * @since Method available since Release 3.1.0 */ function attribute(PHPUnit_Framework_Constraint $constraint, $attributeName) { return call_user_func_array( 'PHPUnit_Framework_Assert::attribute', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsEqual matcher object * that is wrapped in a PHPUnit_Framework_Constraint_Attribute matcher * object. * * @param string $attributeName * @param mixed $value * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase * * @return PHPUnit_Framework_Constraint_Attribute * * @since Method available since Release 3.1.0 */ function attributeEqualTo($attributeName, $value, $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::attributeEqualTo', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_Callback matcher object. * * @param callable $callback * * @return PHPUnit_Framework_Constraint_Callback */ function callback($callback) { return call_user_func_array( 'PHPUnit_Framework_Assert::callback', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_ClassHasAttribute matcher object. * * @param string $attributeName * * @return PHPUnit_Framework_Constraint_ClassHasAttribute * * @since Method available since Release 3.1.0 */ function classHasAttribute($attributeName) { return call_user_func_array( 'PHPUnit_Framework_Assert::classHasAttribute', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_ClassHasStaticAttribute matcher * object. * * @param string $attributeName * * @return PHPUnit_Framework_Constraint_ClassHasStaticAttribute * * @since Method available since Release 3.1.0 */ function classHasStaticAttribute($attributeName) { return call_user_func_array( 'PHPUnit_Framework_Assert::classHasStaticAttribute', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_TraversableContains matcher * object. * * @param mixed $value * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @return PHPUnit_Framework_Constraint_TraversableContains * * @since Method available since Release 3.0.0 */ function contains($value, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::contains', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_TraversableContainsOnly matcher * object. * * @param string $type * * @return PHPUnit_Framework_Constraint_TraversableContainsOnly * * @since Method available since Release 3.1.4 */ function containsOnly($type) { return call_user_func_array( 'PHPUnit_Framework_Assert::containsOnly', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_TraversableContainsOnly matcher * object. * * @param string $classname * * @return PHPUnit_Framework_Constraint_TraversableContainsOnly */ function containsOnlyInstancesOf($classname) { return call_user_func_array( 'PHPUnit_Framework_Assert::containsOnlyInstancesOf', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsEqual matcher object. * * @param mixed $value * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase * * @return PHPUnit_Framework_Constraint_IsEqual * * @since Method available since Release 3.0.0 */ function equalTo($value, $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { return call_user_func_array( 'PHPUnit_Framework_Assert::equalTo', func_get_args() ); } /** * Returns a matcher that matches when the method is executed * exactly $count times. * * @param int $count * * @return PHPUnit_Framework_MockObject_Matcher_InvokedCount * * @since Method available since Release 3.0.0 */ function exactly($count) { return call_user_func_array( 'PHPUnit_Framework_TestCase::exactly', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_FileExists matcher object. * * @return PHPUnit_Framework_Constraint_FileExists * * @since Method available since Release 3.0.0 */ function fileExists() { return call_user_func_array( 'PHPUnit_Framework_Assert::fileExists', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_GreaterThan matcher object. * * @param mixed $value * * @return PHPUnit_Framework_Constraint_GreaterThan * * @since Method available since Release 3.0.0 */ function greaterThan($value) { return call_user_func_array( 'PHPUnit_Framework_Assert::greaterThan', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_Or matcher object that wraps * a PHPUnit_Framework_Constraint_IsEqual and a * PHPUnit_Framework_Constraint_GreaterThan matcher object. * * @param mixed $value * * @return PHPUnit_Framework_Constraint_Or * * @since Method available since Release 3.1.0 */ function greaterThanOrEqual($value) { return call_user_func_array( 'PHPUnit_Framework_Assert::greaterThanOrEqual', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsIdentical matcher object. * * @param mixed $value * * @return PHPUnit_Framework_Constraint_IsIdentical * * @since Method available since Release 3.0.0 */ function identicalTo($value) { return call_user_func_array( 'PHPUnit_Framework_Assert::identicalTo', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsEmpty matcher object. * * @return PHPUnit_Framework_Constraint_IsEmpty * * @since Method available since Release 3.5.0 */ function isEmpty() { return call_user_func_array( 'PHPUnit_Framework_Assert::isEmpty', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsFalse matcher object. * * @return PHPUnit_Framework_Constraint_IsFalse * * @since Method available since Release 3.3.0 */ function isFalse() { return call_user_func_array( 'PHPUnit_Framework_Assert::isFalse', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsInstanceOf matcher object. * * @param string $className * * @return PHPUnit_Framework_Constraint_IsInstanceOf * * @since Method available since Release 3.0.0 */ function isInstanceOf($className) { return call_user_func_array( 'PHPUnit_Framework_Assert::isInstanceOf', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsJson matcher object. * * @return PHPUnit_Framework_Constraint_IsJson * * @since Method available since Release 3.7.20 */ function isJson() { return call_user_func_array( 'PHPUnit_Framework_Assert::isJson', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsNull matcher object. * * @return PHPUnit_Framework_Constraint_IsNull * * @since Method available since Release 3.3.0 */ function isNull() { return call_user_func_array( 'PHPUnit_Framework_Assert::isNull', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsTrue matcher object. * * @return PHPUnit_Framework_Constraint_IsTrue * * @since Method available since Release 3.3.0 */ function isTrue() { return call_user_func_array( 'PHPUnit_Framework_Assert::isTrue', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_IsType matcher object. * * @param string $type * * @return PHPUnit_Framework_Constraint_IsType * * @since Method available since Release 3.0.0 */ function isType($type) { return call_user_func_array( 'PHPUnit_Framework_Assert::isType', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_LessThan matcher object. * * @param mixed $value * * @return PHPUnit_Framework_Constraint_LessThan * * @since Method available since Release 3.0.0 */ function lessThan($value) { return call_user_func_array( 'PHPUnit_Framework_Assert::lessThan', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_Or matcher object that wraps * a PHPUnit_Framework_Constraint_IsEqual and a * PHPUnit_Framework_Constraint_LessThan matcher object. * * @param mixed $value * * @return PHPUnit_Framework_Constraint_Or * * @since Method available since Release 3.1.0 */ function lessThanOrEqual($value) { return call_user_func_array( 'PHPUnit_Framework_Assert::lessThanOrEqual', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_And matcher object. * * @return PHPUnit_Framework_Constraint_And * * @since Method available since Release 3.0.0 */ function logicalAnd() { return call_user_func_array( 'PHPUnit_Framework_Assert::logicalAnd', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_Not matcher object. * * @param PHPUnit_Framework_Constraint $constraint * * @return PHPUnit_Framework_Constraint_Not * * @since Method available since Release 3.0.0 */ function logicalNot(PHPUnit_Framework_Constraint $constraint) { return call_user_func_array( 'PHPUnit_Framework_Assert::logicalNot', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_Or matcher object. * * @return PHPUnit_Framework_Constraint_Or * * @since Method available since Release 3.0.0 */ function logicalOr() { return call_user_func_array( 'PHPUnit_Framework_Assert::logicalOr', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_Xor matcher object. * * @return PHPUnit_Framework_Constraint_Xor * * @since Method available since Release 3.0.0 */ function logicalXor() { return call_user_func_array( 'PHPUnit_Framework_Assert::logicalXor', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_StringMatches matcher object. * * @param string $string * * @return PHPUnit_Framework_Constraint_StringMatches * * @since Method available since Release 3.5.0 */ function matches($string) { return call_user_func_array( 'PHPUnit_Framework_Assert::matches', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_PCREMatch matcher object. * * @param string $pattern * * @return PHPUnit_Framework_Constraint_PCREMatch * * @since Method available since Release 3.0.0 */ function matchesRegularExpression($pattern) { return call_user_func_array( 'PHPUnit_Framework_Assert::matchesRegularExpression', func_get_args() ); } /** * Returns a matcher that matches when the method is never executed. * * @return PHPUnit_Framework_MockObject_Matcher_InvokedCount * * @since Method available since Release 3.0.0 */ function never() { return call_user_func_array( 'PHPUnit_Framework_TestCase::never', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_ObjectHasAttribute matcher object. * * @param string $attributeName * * @return PHPUnit_Framework_Constraint_ObjectHasAttribute * * @since Method available since Release 3.0.0 */ function objectHasAttribute($attributeName) { return call_user_func_array( 'PHPUnit_Framework_Assert::objectHasAttribute', func_get_args() ); } /** * @param mixed $value, ... * * @return PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls * * @since Method available since Release 3.0.0 */ function onConsecutiveCalls() { return call_user_func_array( 'PHPUnit_Framework_TestCase::onConsecutiveCalls', func_get_args() ); } /** * Returns a matcher that matches when the method is executed exactly once. * * @return PHPUnit_Framework_MockObject_Matcher_InvokedCount * * @since Method available since Release 3.0.0 */ function once() { return call_user_func_array( 'PHPUnit_Framework_TestCase::once', func_get_args() ); } /** * @param int $argumentIndex * * @return PHPUnit_Framework_MockObject_Stub_ReturnArgument * * @since Method available since Release 3.3.0 */ function returnArgument($argumentIndex) { return call_user_func_array( 'PHPUnit_Framework_TestCase::returnArgument', func_get_args() ); } /** * @param mixed $callback * * @return PHPUnit_Framework_MockObject_Stub_ReturnCallback * * @since Method available since Release 3.3.0 */ function returnCallback($callback) { return call_user_func_array( 'PHPUnit_Framework_TestCase::returnCallback', func_get_args() ); } /** * Returns the current object. * * This method is useful when mocking a fluent interface. * * @return PHPUnit_Framework_MockObject_Stub_ReturnSelf * * @since Method available since Release 3.6.0 */ function returnSelf() { return call_user_func_array( 'PHPUnit_Framework_TestCase::returnSelf', func_get_args() ); } /** * @param mixed $value * * @return PHPUnit_Framework_MockObject_Stub_Return * * @since Method available since Release 3.0.0 */ function returnValue($value) { return call_user_func_array( 'PHPUnit_Framework_TestCase::returnValue', func_get_args() ); } /** * @param array $valueMap * * @return PHPUnit_Framework_MockObject_Stub_ReturnValueMap * * @since Method available since Release 3.6.0 */ function returnValueMap(array $valueMap) { return call_user_func_array( 'PHPUnit_Framework_TestCase::returnValueMap', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_StringContains matcher object. * * @param string $string * @param bool $case * * @return PHPUnit_Framework_Constraint_StringContains * * @since Method available since Release 3.0.0 */ function stringContains($string, $case = true) { return call_user_func_array( 'PHPUnit_Framework_Assert::stringContains', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_StringEndsWith matcher object. * * @param mixed $suffix * * @return PHPUnit_Framework_Constraint_StringEndsWith * * @since Method available since Release 3.4.0 */ function stringEndsWith($suffix) { return call_user_func_array( 'PHPUnit_Framework_Assert::stringEndsWith', func_get_args() ); } /** * Returns a PHPUnit_Framework_Constraint_StringStartsWith matcher object. * * @param mixed $prefix * * @return PHPUnit_Framework_Constraint_StringStartsWith * * @since Method available since Release 3.4.0 */ function stringStartsWith($prefix) { return call_user_func_array( 'PHPUnit_Framework_Assert::stringStartsWith', func_get_args() ); } /** * @param Exception $exception * * @return PHPUnit_Framework_MockObject_Stub_Exception * * @since Method available since Release 3.1.0 */ function throwException(Exception $exception) { return call_user_func_array( 'PHPUnit_Framework_TestCase::throwException', func_get_args() ); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A set of assertion methods. * * @since Class available since Release 2.0.0 */ abstract class PHPUnit_Framework_Assert { /** * @var int */ private static $count = 0; /** * Asserts that an array has a specified key. * * @param mixed $key * @param array|ArrayAccess $array * @param string $message * * @since Method available since Release 3.0.0 */ public static function assertArrayHasKey($key, $array, $message = '') { if (!(is_integer($key) || is_string($key))) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 1, 'integer or string' ); } if (!(is_array($array) || $array instanceof ArrayAccess)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 2, 'array or ArrayAccess' ); } $constraint = new PHPUnit_Framework_Constraint_ArrayHasKey($key); self::assertThat($array, $constraint, $message); } /** * Asserts that an array has a specified subset. * * @param array|ArrayAccess $subset * @param array|ArrayAccess $array * @param bool $strict Check for object identity * @param string $message * * @since Method available since Release 4.4.0 */ public static function assertArraySubset($subset, $array, $strict = false, $message = '') { if (!is_array($subset)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 1, 'array or ArrayAccess' ); } if (!is_array($array)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 2, 'array or ArrayAccess' ); } $constraint = new PHPUnit_Framework_Constraint_ArraySubset($subset, $strict); self::assertThat($array, $constraint, $message); } /** * Asserts that an array does not have a specified key. * * @param mixed $key * @param array|ArrayAccess $array * @param string $message * * @since Method available since Release 3.0.0 */ public static function assertArrayNotHasKey($key, $array, $message = '') { if (!(is_integer($key) || is_string($key))) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 1, 'integer or string' ); } if (!(is_array($array) || $array instanceof ArrayAccess)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 2, 'array or ArrayAccess' ); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_ArrayHasKey($key) ); self::assertThat($array, $constraint, $message); } /** * Asserts that a haystack contains a needle. * * @param mixed $needle * @param mixed $haystack * @param string $message * @param bool $ignoreCase * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @since Method available since Release 2.1.0 */ public static function assertContains($needle, $haystack, $message = '', $ignoreCase = false, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { if (is_array($haystack) || is_object($haystack) && $haystack instanceof Traversable) { $constraint = new PHPUnit_Framework_Constraint_TraversableContains( $needle, $checkForObjectIdentity, $checkForNonObjectIdentity ); } elseif (is_string($haystack)) { if (!is_string($needle)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 1, 'string' ); } $constraint = new PHPUnit_Framework_Constraint_StringContains( $needle, $ignoreCase ); } else { throw PHPUnit_Util_InvalidArgumentHelper::factory( 2, 'array, traversable or string' ); } self::assertThat($haystack, $constraint, $message); } /** * Asserts that a haystack that is stored in a static attribute of a class * or an attribute of an object contains a needle. * * @param mixed $needle * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * @param bool $ignoreCase * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @since Method available since Release 3.0.0 */ public static function assertAttributeContains($needle, $haystackAttributeName, $haystackClassOrObject, $message = '', $ignoreCase = false, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { self::assertContains( $needle, self::readAttribute($haystackClassOrObject, $haystackAttributeName), $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity ); } /** * Asserts that a haystack does not contain a needle. * * @param mixed $needle * @param mixed $haystack * @param string $message * @param bool $ignoreCase * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @since Method available since Release 2.1.0 */ public static function assertNotContains($needle, $haystack, $message = '', $ignoreCase = false, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { if (is_array($haystack) || is_object($haystack) && $haystack instanceof Traversable) { $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_TraversableContains( $needle, $checkForObjectIdentity, $checkForNonObjectIdentity ) ); } elseif (is_string($haystack)) { if (!is_string($needle)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 1, 'string' ); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_StringContains( $needle, $ignoreCase ) ); } else { throw PHPUnit_Util_InvalidArgumentHelper::factory( 2, 'array, traversable or string' ); } self::assertThat($haystack, $constraint, $message); } /** * Asserts that a haystack that is stored in a static attribute of a class * or an attribute of an object does not contain a needle. * * @param mixed $needle * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * @param bool $ignoreCase * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @since Method available since Release 3.0.0 */ public static function assertAttributeNotContains($needle, $haystackAttributeName, $haystackClassOrObject, $message = '', $ignoreCase = false, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { self::assertNotContains( $needle, self::readAttribute($haystackClassOrObject, $haystackAttributeName), $message, $ignoreCase, $checkForObjectIdentity, $checkForNonObjectIdentity ); } /** * Asserts that a haystack contains only values of a given type. * * @param string $type * @param mixed $haystack * @param bool $isNativeType * @param string $message * * @since Method available since Release 3.1.4 */ public static function assertContainsOnly($type, $haystack, $isNativeType = null, $message = '') { if (!(is_array($haystack) || is_object($haystack) && $haystack instanceof Traversable)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 2, 'array or traversable' ); } if ($isNativeType == null) { $isNativeType = PHPUnit_Util_Type::isType($type); } self::assertThat( $haystack, new PHPUnit_Framework_Constraint_TraversableContainsOnly( $type, $isNativeType ), $message ); } /** * Asserts that a haystack contains only instances of a given classname * * @param string $classname * @param array|Traversable $haystack * @param string $message */ public static function assertContainsOnlyInstancesOf($classname, $haystack, $message = '') { if (!(is_array($haystack) || is_object($haystack) && $haystack instanceof Traversable)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 2, 'array or traversable' ); } self::assertThat( $haystack, new PHPUnit_Framework_Constraint_TraversableContainsOnly( $classname, false ), $message ); } /** * Asserts that a haystack that is stored in a static attribute of a class * or an attribute of an object contains only values of a given type. * * @param string $type * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param bool $isNativeType * @param string $message * * @since Method available since Release 3.1.4 */ public static function assertAttributeContainsOnly($type, $haystackAttributeName, $haystackClassOrObject, $isNativeType = null, $message = '') { self::assertContainsOnly( $type, self::readAttribute($haystackClassOrObject, $haystackAttributeName), $isNativeType, $message ); } /** * Asserts that a haystack does not contain only values of a given type. * * @param string $type * @param mixed $haystack * @param bool $isNativeType * @param string $message * * @since Method available since Release 3.1.4 */ public static function assertNotContainsOnly($type, $haystack, $isNativeType = null, $message = '') { if (!(is_array($haystack) || is_object($haystack) && $haystack instanceof Traversable)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 2, 'array or traversable' ); } if ($isNativeType == null) { $isNativeType = PHPUnit_Util_Type::isType($type); } self::assertThat( $haystack, new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_TraversableContainsOnly( $type, $isNativeType ) ), $message ); } /** * Asserts that a haystack that is stored in a static attribute of a class * or an attribute of an object does not contain only values of a given * type. * * @param string $type * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param bool $isNativeType * @param string $message * * @since Method available since Release 3.1.4 */ public static function assertAttributeNotContainsOnly($type, $haystackAttributeName, $haystackClassOrObject, $isNativeType = null, $message = '') { self::assertNotContainsOnly( $type, self::readAttribute($haystackClassOrObject, $haystackAttributeName), $isNativeType, $message ); } /** * Asserts the number of elements of an array, Countable or Traversable. * * @param int $expectedCount * @param mixed $haystack * @param string $message */ public static function assertCount($expectedCount, $haystack, $message = '') { if (!is_int($expectedCount)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'integer'); } if (!$haystack instanceof Countable && !$haystack instanceof Traversable && !is_array($haystack)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'countable or traversable'); } self::assertThat( $haystack, new PHPUnit_Framework_Constraint_Count($expectedCount), $message ); } /** * Asserts the number of elements of an array, Countable or Traversable * that is stored in an attribute. * * @param int $expectedCount * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * * @since Method available since Release 3.6.0 */ public static function assertAttributeCount($expectedCount, $haystackAttributeName, $haystackClassOrObject, $message = '') { self::assertCount( $expectedCount, self::readAttribute($haystackClassOrObject, $haystackAttributeName), $message ); } /** * Asserts the number of elements of an array, Countable or Traversable. * * @param int $expectedCount * @param mixed $haystack * @param string $message */ public static function assertNotCount($expectedCount, $haystack, $message = '') { if (!is_int($expectedCount)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'integer'); } if (!$haystack instanceof Countable && !$haystack instanceof Traversable && !is_array($haystack)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'countable or traversable'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_Count($expectedCount) ); self::assertThat($haystack, $constraint, $message); } /** * Asserts the number of elements of an array, Countable or Traversable * that is stored in an attribute. * * @param int $expectedCount * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * * @since Method available since Release 3.6.0 */ public static function assertAttributeNotCount($expectedCount, $haystackAttributeName, $haystackClassOrObject, $message = '') { self::assertNotCount( $expectedCount, self::readAttribute($haystackClassOrObject, $haystackAttributeName), $message ); } /** * Asserts that two variables are equal. * * @param mixed $expected * @param mixed $actual * @param string $message * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase */ public static function assertEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { $constraint = new PHPUnit_Framework_Constraint_IsEqual( $expected, $delta, $maxDepth, $canonicalize, $ignoreCase ); self::assertThat($actual, $constraint, $message); } /** * Asserts that a variable is equal to an attribute of an object. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase */ public static function assertAttributeEquals($expected, $actualAttributeName, $actualClassOrObject, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { self::assertEquals( $expected, self::readAttribute($actualClassOrObject, $actualAttributeName), $message, $delta, $maxDepth, $canonicalize, $ignoreCase ); } /** * Asserts that two variables are not equal. * * @param mixed $expected * @param mixed $actual * @param string $message * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase * * @since Method available since Release 2.3.0 */ public static function assertNotEquals($expected, $actual, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_IsEqual( $expected, $delta, $maxDepth, $canonicalize, $ignoreCase ) ); self::assertThat($actual, $constraint, $message); } /** * Asserts that a variable is not equal to an attribute of an object. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase */ public static function assertAttributeNotEquals($expected, $actualAttributeName, $actualClassOrObject, $message = '', $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { self::assertNotEquals( $expected, self::readAttribute($actualClassOrObject, $actualAttributeName), $message, $delta, $maxDepth, $canonicalize, $ignoreCase ); } /** * Asserts that a variable is empty. * * @param mixed $actual * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ public static function assertEmpty($actual, $message = '') { self::assertThat($actual, self::isEmpty(), $message); } /** * Asserts that a static attribute of a class or an attribute of an object * is empty. * * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertAttributeEmpty($haystackAttributeName, $haystackClassOrObject, $message = '') { self::assertEmpty( self::readAttribute($haystackClassOrObject, $haystackAttributeName), $message ); } /** * Asserts that a variable is not empty. * * @param mixed $actual * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ public static function assertNotEmpty($actual, $message = '') { self::assertThat($actual, self::logicalNot(self::isEmpty()), $message); } /** * Asserts that a static attribute of a class or an attribute of an object * is not empty. * * @param string $haystackAttributeName * @param mixed $haystackClassOrObject * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertAttributeNotEmpty($haystackAttributeName, $haystackClassOrObject, $message = '') { self::assertNotEmpty( self::readAttribute($haystackClassOrObject, $haystackAttributeName), $message ); } /** * Asserts that a value is greater than another value. * * @param mixed $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertGreaterThan($expected, $actual, $message = '') { self::assertThat($actual, self::greaterThan($expected), $message); } /** * Asserts that an attribute is greater than another value. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertAttributeGreaterThan($expected, $actualAttributeName, $actualClassOrObject, $message = '') { self::assertGreaterThan( $expected, self::readAttribute($actualClassOrObject, $actualAttributeName), $message ); } /** * Asserts that a value is greater than or equal to another value. * * @param mixed $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertGreaterThanOrEqual($expected, $actual, $message = '') { self::assertThat( $actual, self::greaterThanOrEqual($expected), $message ); } /** * Asserts that an attribute is greater than or equal to another value. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertAttributeGreaterThanOrEqual($expected, $actualAttributeName, $actualClassOrObject, $message = '') { self::assertGreaterThanOrEqual( $expected, self::readAttribute($actualClassOrObject, $actualAttributeName), $message ); } /** * Asserts that a value is smaller than another value. * * @param mixed $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertLessThan($expected, $actual, $message = '') { self::assertThat($actual, self::lessThan($expected), $message); } /** * Asserts that an attribute is smaller than another value. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertAttributeLessThan($expected, $actualAttributeName, $actualClassOrObject, $message = '') { self::assertLessThan( $expected, self::readAttribute($actualClassOrObject, $actualAttributeName), $message ); } /** * Asserts that a value is smaller than or equal to another value. * * @param mixed $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertLessThanOrEqual($expected, $actual, $message = '') { self::assertThat($actual, self::lessThanOrEqual($expected), $message); } /** * Asserts that an attribute is smaller than or equal to another value. * * @param mixed $expected * @param string $actualAttributeName * @param string $actualClassOrObject * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertAttributeLessThanOrEqual($expected, $actualAttributeName, $actualClassOrObject, $message = '') { self::assertLessThanOrEqual( $expected, self::readAttribute($actualClassOrObject, $actualAttributeName), $message ); } /** * Asserts that the contents of one file is equal to the contents of another * file. * * @param string $expected * @param string $actual * @param string $message * @param bool $canonicalize * @param bool $ignoreCase * * @since Method available since Release 3.2.14 */ public static function assertFileEquals($expected, $actual, $message = '', $canonicalize = false, $ignoreCase = false) { self::assertFileExists($expected, $message); self::assertFileExists($actual, $message); self::assertEquals( file_get_contents($expected), file_get_contents($actual), $message, 0, 10, $canonicalize, $ignoreCase ); } /** * Asserts that the contents of one file is not equal to the contents of * another file. * * @param string $expected * @param string $actual * @param string $message * @param bool $canonicalize * @param bool $ignoreCase * * @since Method available since Release 3.2.14 */ public static function assertFileNotEquals($expected, $actual, $message = '', $canonicalize = false, $ignoreCase = false) { self::assertFileExists($expected, $message); self::assertFileExists($actual, $message); self::assertNotEquals( file_get_contents($expected), file_get_contents($actual), $message, 0, 10, $canonicalize, $ignoreCase ); } /** * Asserts that the contents of a string is equal * to the contents of a file. * * @param string $expectedFile * @param string $actualString * @param string $message * @param bool $canonicalize * @param bool $ignoreCase * * @since Method available since Release 3.3.0 */ public static function assertStringEqualsFile($expectedFile, $actualString, $message = '', $canonicalize = false, $ignoreCase = false) { self::assertFileExists($expectedFile, $message); self::assertEquals( file_get_contents($expectedFile), $actualString, $message, 0, 10, $canonicalize, $ignoreCase ); } /** * Asserts that the contents of a string is not equal * to the contents of a file. * * @param string $expectedFile * @param string $actualString * @param string $message * @param bool $canonicalize * @param bool $ignoreCase * * @since Method available since Release 3.3.0 */ public static function assertStringNotEqualsFile($expectedFile, $actualString, $message = '', $canonicalize = false, $ignoreCase = false) { self::assertFileExists($expectedFile, $message); self::assertNotEquals( file_get_contents($expectedFile), $actualString, $message, 0, 10, $canonicalize, $ignoreCase ); } /** * Asserts that a file exists. * * @param string $filename * @param string $message * * @since Method available since Release 3.0.0 */ public static function assertFileExists($filename, $message = '') { if (!is_string($filename)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } $constraint = new PHPUnit_Framework_Constraint_FileExists; self::assertThat($filename, $constraint, $message); } /** * Asserts that a file does not exist. * * @param string $filename * @param string $message * * @since Method available since Release 3.0.0 */ public static function assertFileNotExists($filename, $message = '') { if (!is_string($filename)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_FileExists ); self::assertThat($filename, $constraint, $message); } /** * Asserts that a condition is true. * * @param bool $condition * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ public static function assertTrue($condition, $message = '') { self::assertThat($condition, self::isTrue(), $message); } /** * Asserts that a condition is not true. * * @param bool $condition * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ public static function assertNotTrue($condition, $message = '') { self::assertThat($condition, self::logicalNot(self::isTrue()), $message); } /** * Asserts that a condition is false. * * @param bool $condition * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ public static function assertFalse($condition, $message = '') { self::assertThat($condition, self::isFalse(), $message); } /** * Asserts that a condition is not false. * * @param bool $condition * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ public static function assertNotFalse($condition, $message = '') { self::assertThat($condition, self::logicalNot(self::isFalse()), $message); } /** * Asserts that a variable is not null. * * @param mixed $actual * @param string $message */ public static function assertNotNull($actual, $message = '') { self::assertThat($actual, self::logicalNot(self::isNull()), $message); } /** * Asserts that a variable is null. * * @param mixed $actual * @param string $message */ public static function assertNull($actual, $message = '') { self::assertThat($actual, self::isNull(), $message); } /** * Asserts that a class has a specified attribute. * * @param string $attributeName * @param string $className * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertClassHasAttribute($attributeName, $className, $message = '') { if (!is_string($attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'valid attribute name'); } if (!is_string($className) || !class_exists($className)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'class name', $className); } $constraint = new PHPUnit_Framework_Constraint_ClassHasAttribute( $attributeName ); self::assertThat($className, $constraint, $message); } /** * Asserts that a class does not have a specified attribute. * * @param string $attributeName * @param string $className * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertClassNotHasAttribute($attributeName, $className, $message = '') { if (!is_string($attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'valid attribute name'); } if (!is_string($className) || !class_exists($className)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'class name', $className); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_ClassHasAttribute($attributeName) ); self::assertThat($className, $constraint, $message); } /** * Asserts that a class has a specified static attribute. * * @param string $attributeName * @param string $className * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertClassHasStaticAttribute($attributeName, $className, $message = '') { if (!is_string($attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'valid attribute name'); } if (!is_string($className) || !class_exists($className)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'class name', $className); } $constraint = new PHPUnit_Framework_Constraint_ClassHasStaticAttribute( $attributeName ); self::assertThat($className, $constraint, $message); } /** * Asserts that a class does not have a specified static attribute. * * @param string $attributeName * @param string $className * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertClassNotHasStaticAttribute($attributeName, $className, $message = '') { if (!is_string($attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'valid attribute name'); } if (!is_string($className) || !class_exists($className)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'class name', $className); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_ClassHasStaticAttribute( $attributeName ) ); self::assertThat($className, $constraint, $message); } /** * Asserts that an object has a specified attribute. * * @param string $attributeName * @param object $object * @param string $message * * @since Method available since Release 3.0.0 */ public static function assertObjectHasAttribute($attributeName, $object, $message = '') { if (!is_string($attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'valid attribute name'); } if (!is_object($object)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'object'); } $constraint = new PHPUnit_Framework_Constraint_ObjectHasAttribute( $attributeName ); self::assertThat($object, $constraint, $message); } /** * Asserts that an object does not have a specified attribute. * * @param string $attributeName * @param object $object * @param string $message * * @since Method available since Release 3.0.0 */ public static function assertObjectNotHasAttribute($attributeName, $object, $message = '') { if (!is_string($attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'valid attribute name'); } if (!is_object($object)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'object'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_ObjectHasAttribute($attributeName) ); self::assertThat($object, $constraint, $message); } /** * Asserts that two variables have the same type and value. * Used on objects, it asserts that two variables reference * the same object. * * @param mixed $expected * @param mixed $actual * @param string $message */ public static function assertSame($expected, $actual, $message = '') { if (is_bool($expected) && is_bool($actual)) { self::assertEquals($expected, $actual, $message); } else { $constraint = new PHPUnit_Framework_Constraint_IsIdentical( $expected ); self::assertThat($actual, $constraint, $message); } } /** * Asserts that a variable and an attribute of an object have the same type * and value. * * @param mixed $expected * @param string $actualAttributeName * @param object $actualClassOrObject * @param string $message */ public static function assertAttributeSame($expected, $actualAttributeName, $actualClassOrObject, $message = '') { self::assertSame( $expected, self::readAttribute($actualClassOrObject, $actualAttributeName), $message ); } /** * Asserts that two variables do not have the same type and value. * Used on objects, it asserts that two variables do not reference * the same object. * * @param mixed $expected * @param mixed $actual * @param string $message */ public static function assertNotSame($expected, $actual, $message = '') { if (is_bool($expected) && is_bool($actual)) { self::assertNotEquals($expected, $actual, $message); } else { $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_IsIdentical($expected) ); self::assertThat($actual, $constraint, $message); } } /** * Asserts that a variable and an attribute of an object do not have the * same type and value. * * @param mixed $expected * @param string $actualAttributeName * @param object $actualClassOrObject * @param string $message */ public static function assertAttributeNotSame($expected, $actualAttributeName, $actualClassOrObject, $message = '') { self::assertNotSame( $expected, self::readAttribute($actualClassOrObject, $actualAttributeName), $message ); } /** * Asserts that a variable is of a given type. * * @param string $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertInstanceOf($expected, $actual, $message = '') { if (!(is_string($expected) && (class_exists($expected) || interface_exists($expected)))) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'class or interface name'); } $constraint = new PHPUnit_Framework_Constraint_IsInstanceOf( $expected ); self::assertThat($actual, $constraint, $message); } /** * Asserts that an attribute is of a given type. * * @param string $expected * @param string $attributeName * @param mixed $classOrObject * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertAttributeInstanceOf($expected, $attributeName, $classOrObject, $message = '') { self::assertInstanceOf( $expected, self::readAttribute($classOrObject, $attributeName), $message ); } /** * Asserts that a variable is not of a given type. * * @param string $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertNotInstanceOf($expected, $actual, $message = '') { if (!(is_string($expected) && (class_exists($expected) || interface_exists($expected)))) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'class or interface name'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_IsInstanceOf($expected) ); self::assertThat($actual, $constraint, $message); } /** * Asserts that an attribute is of a given type. * * @param string $expected * @param string $attributeName * @param mixed $classOrObject * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertAttributeNotInstanceOf($expected, $attributeName, $classOrObject, $message = '') { self::assertNotInstanceOf( $expected, self::readAttribute($classOrObject, $attributeName), $message ); } /** * Asserts that a variable is of a given type. * * @param string $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertInternalType($expected, $actual, $message = '') { if (!is_string($expected)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } $constraint = new PHPUnit_Framework_Constraint_IsType( $expected ); self::assertThat($actual, $constraint, $message); } /** * Asserts that an attribute is of a given type. * * @param string $expected * @param string $attributeName * @param mixed $classOrObject * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertAttributeInternalType($expected, $attributeName, $classOrObject, $message = '') { self::assertInternalType( $expected, self::readAttribute($classOrObject, $attributeName), $message ); } /** * Asserts that a variable is not of a given type. * * @param string $expected * @param mixed $actual * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertNotInternalType($expected, $actual, $message = '') { if (!is_string($expected)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_IsType($expected) ); self::assertThat($actual, $constraint, $message); } /** * Asserts that an attribute is of a given type. * * @param string $expected * @param string $attributeName * @param mixed $classOrObject * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertAttributeNotInternalType($expected, $attributeName, $classOrObject, $message = '') { self::assertNotInternalType( $expected, self::readAttribute($classOrObject, $attributeName), $message ); } /** * Asserts that a string matches a given regular expression. * * @param string $pattern * @param string $string * @param string $message */ public static function assertRegExp($pattern, $string, $message = '') { if (!is_string($pattern)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($string)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } $constraint = new PHPUnit_Framework_Constraint_PCREMatch($pattern); self::assertThat($string, $constraint, $message); } /** * Asserts that a string does not match a given regular expression. * * @param string $pattern * @param string $string * @param string $message * * @since Method available since Release 2.1.0 */ public static function assertNotRegExp($pattern, $string, $message = '') { if (!is_string($pattern)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($string)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_PCREMatch($pattern) ); self::assertThat($string, $constraint, $message); } /** * Assert that the size of two arrays (or `Countable` or `Traversable` objects) * is the same. * * @param array|Countable|Traversable $expected * @param array|Countable|Traversable $actual * @param string $message */ public static function assertSameSize($expected, $actual, $message = '') { if (!$expected instanceof Countable && !$expected instanceof Traversable && !is_array($expected)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'countable or traversable'); } if (!$actual instanceof Countable && !$actual instanceof Traversable && !is_array($actual)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'countable or traversable'); } self::assertThat( $actual, new PHPUnit_Framework_Constraint_SameSize($expected), $message ); } /** * Assert that the size of two arrays (or `Countable` or `Traversable` objects) * is not the same. * * @param array|Countable|Traversable $expected * @param array|Countable|Traversable $actual * @param string $message */ public static function assertNotSameSize($expected, $actual, $message = '') { if (!$expected instanceof Countable && !$expected instanceof Traversable && !is_array($expected)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'countable or traversable'); } if (!$actual instanceof Countable && !$actual instanceof Traversable && !is_array($actual)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'countable or traversable'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_SameSize($expected) ); self::assertThat($actual, $constraint, $message); } /** * Asserts that a string matches a given format string. * * @param string $format * @param string $string * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertStringMatchesFormat($format, $string, $message = '') { if (!is_string($format)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($string)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } $constraint = new PHPUnit_Framework_Constraint_StringMatches($format); self::assertThat($string, $constraint, $message); } /** * Asserts that a string does not match a given format string. * * @param string $format * @param string $string * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertStringNotMatchesFormat($format, $string, $message = '') { if (!is_string($format)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($string)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_StringMatches($format) ); self::assertThat($string, $constraint, $message); } /** * Asserts that a string matches a given format file. * * @param string $formatFile * @param string $string * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertStringMatchesFormatFile($formatFile, $string, $message = '') { self::assertFileExists($formatFile, $message); if (!is_string($string)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } $constraint = new PHPUnit_Framework_Constraint_StringMatches( file_get_contents($formatFile) ); self::assertThat($string, $constraint, $message); } /** * Asserts that a string does not match a given format string. * * @param string $formatFile * @param string $string * @param string $message * * @since Method available since Release 3.5.0 */ public static function assertStringNotMatchesFormatFile($formatFile, $string, $message = '') { self::assertFileExists($formatFile, $message); if (!is_string($string)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_StringMatches( file_get_contents($formatFile) ) ); self::assertThat($string, $constraint, $message); } /** * Asserts that a string starts with a given prefix. * * @param string $prefix * @param string $string * @param string $message * * @since Method available since Release 3.4.0 */ public static function assertStringStartsWith($prefix, $string, $message = '') { if (!is_string($prefix)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($string)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } $constraint = new PHPUnit_Framework_Constraint_StringStartsWith( $prefix ); self::assertThat($string, $constraint, $message); } /** * Asserts that a string starts not with a given prefix. * * @param string $prefix * @param string $string * @param string $message * * @since Method available since Release 3.4.0 */ public static function assertStringStartsNotWith($prefix, $string, $message = '') { if (!is_string($prefix)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($string)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_StringStartsWith($prefix) ); self::assertThat($string, $constraint, $message); } /** * Asserts that a string ends with a given suffix. * * @param string $suffix * @param string $string * @param string $message * * @since Method available since Release 3.4.0 */ public static function assertStringEndsWith($suffix, $string, $message = '') { if (!is_string($suffix)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($string)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } $constraint = new PHPUnit_Framework_Constraint_StringEndsWith($suffix); self::assertThat($string, $constraint, $message); } /** * Asserts that a string ends not with a given suffix. * * @param string $suffix * @param string $string * @param string $message * * @since Method available since Release 3.4.0 */ public static function assertStringEndsNotWith($suffix, $string, $message = '') { if (!is_string($suffix)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($string)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } $constraint = new PHPUnit_Framework_Constraint_Not( new PHPUnit_Framework_Constraint_StringEndsWith($suffix) ); self::assertThat($string, $constraint, $message); } /** * Asserts that two XML files are equal. * * @param string $expectedFile * @param string $actualFile * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertXmlFileEqualsXmlFile($expectedFile, $actualFile, $message = '') { $expected = PHPUnit_Util_XML::loadFile($expectedFile); $actual = PHPUnit_Util_XML::loadFile($actualFile); self::assertEquals($expected, $actual, $message); } /** * Asserts that two XML files are not equal. * * @param string $expectedFile * @param string $actualFile * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertXmlFileNotEqualsXmlFile($expectedFile, $actualFile, $message = '') { $expected = PHPUnit_Util_XML::loadFile($expectedFile); $actual = PHPUnit_Util_XML::loadFile($actualFile); self::assertNotEquals($expected, $actual, $message); } /** * Asserts that two XML documents are equal. * * @param string $expectedFile * @param string $actualXml * @param string $message * * @since Method available since Release 3.3.0 */ public static function assertXmlStringEqualsXmlFile($expectedFile, $actualXml, $message = '') { $expected = PHPUnit_Util_XML::loadFile($expectedFile); $actual = PHPUnit_Util_XML::load($actualXml); self::assertEquals($expected, $actual, $message); } /** * Asserts that two XML documents are not equal. * * @param string $expectedFile * @param string $actualXml * @param string $message * * @since Method available since Release 3.3.0 */ public static function assertXmlStringNotEqualsXmlFile($expectedFile, $actualXml, $message = '') { $expected = PHPUnit_Util_XML::loadFile($expectedFile); $actual = PHPUnit_Util_XML::load($actualXml); self::assertNotEquals($expected, $actual, $message); } /** * Asserts that two XML documents are equal. * * @param string $expectedXml * @param string $actualXml * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertXmlStringEqualsXmlString($expectedXml, $actualXml, $message = '') { $expected = PHPUnit_Util_XML::load($expectedXml); $actual = PHPUnit_Util_XML::load($actualXml); self::assertEquals($expected, $actual, $message); } /** * Asserts that two XML documents are not equal. * * @param string $expectedXml * @param string $actualXml * @param string $message * * @since Method available since Release 3.1.0 */ public static function assertXmlStringNotEqualsXmlString($expectedXml, $actualXml, $message = '') { $expected = PHPUnit_Util_XML::load($expectedXml); $actual = PHPUnit_Util_XML::load($actualXml); self::assertNotEquals($expected, $actual, $message); } /** * Asserts that a hierarchy of DOMElements matches. * * @param DOMElement $expectedElement * @param DOMElement $actualElement * @param bool $checkAttributes * @param string $message * * @since Method available since Release 3.3.0 */ public static function assertEqualXMLStructure(DOMElement $expectedElement, DOMElement $actualElement, $checkAttributes = false, $message = '') { $tmp = new DOMDocument; $expectedElement = $tmp->importNode($expectedElement, true); $tmp = new DOMDocument; $actualElement = $tmp->importNode($actualElement, true); unset($tmp); self::assertEquals( $expectedElement->tagName, $actualElement->tagName, $message ); if ($checkAttributes) { self::assertEquals( $expectedElement->attributes->length, $actualElement->attributes->length, sprintf( '%s%sNumber of attributes on node "%s" does not match', $message, !empty($message) ? "\n" : '', $expectedElement->tagName ) ); for ($i = 0; $i < $expectedElement->attributes->length; $i++) { $expectedAttribute = $expectedElement->attributes->item($i); $actualAttribute = $actualElement->attributes->getNamedItem( $expectedAttribute->name ); if (!$actualAttribute) { self::fail( sprintf( '%s%sCould not find attribute "%s" on node "%s"', $message, !empty($message) ? "\n" : '', $expectedAttribute->name, $expectedElement->tagName ) ); } } } PHPUnit_Util_XML::removeCharacterDataNodes($expectedElement); PHPUnit_Util_XML::removeCharacterDataNodes($actualElement); self::assertEquals( $expectedElement->childNodes->length, $actualElement->childNodes->length, sprintf( '%s%sNumber of child nodes of "%s" differs', $message, !empty($message) ? "\n" : '', $expectedElement->tagName ) ); for ($i = 0; $i < $expectedElement->childNodes->length; $i++) { self::assertEqualXMLStructure( $expectedElement->childNodes->item($i), $actualElement->childNodes->item($i), $checkAttributes, $message ); } } /** * Assert the presence, absence, or count of elements in a document matching * the CSS $selector, regardless of the contents of those elements. * * The first argument, $selector, is the CSS selector used to match * the elements in the $actual document. * * The second argument, $count, can be either boolean or numeric. * When boolean, it asserts for presence of elements matching the selector * (true) or absence of elements (false). * When numeric, it asserts the count of elements. * * assertSelectCount("#binder", true, $xml); // any? * assertSelectCount(".binder", 3, $xml); // exactly 3? * * @param array $selector * @param int|bool|array $count * @param mixed $actual * @param string $message * @param bool $isHtml * * @since Method available since Release 3.3.0 * @deprecated * @codeCoverageIgnore */ public static function assertSelectCount($selector, $count, $actual, $message = '', $isHtml = true) { trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED); self::assertSelectEquals( $selector, true, $count, $actual, $message, $isHtml ); } /** * assertSelectRegExp("#binder .name", "/Mike|Derek/", true, $xml); // any? * assertSelectRegExp("#binder .name", "/Mike|Derek/", 3, $xml); // 3? * * @param array $selector * @param string $pattern * @param int|bool|array $count * @param mixed $actual * @param string $message * @param bool $isHtml * * @since Method available since Release 3.3.0 * @deprecated * @codeCoverageIgnore */ public static function assertSelectRegExp($selector, $pattern, $count, $actual, $message = '', $isHtml = true) { trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED); self::assertSelectEquals( $selector, "regexp:$pattern", $count, $actual, $message, $isHtml ); } /** * assertSelectEquals("#binder .name", "Chuck", true, $xml); // any? * assertSelectEquals("#binder .name", "Chuck", false, $xml); // none? * * @param array $selector * @param string $content * @param int|bool|array $count * @param mixed $actual * @param string $message * @param bool $isHtml * * @since Method available since Release 3.3.0 * @deprecated * @codeCoverageIgnore */ public static function assertSelectEquals($selector, $content, $count, $actual, $message = '', $isHtml = true) { trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED); $tags = PHPUnit_Util_XML::cssSelect( $selector, $content, $actual, $isHtml ); // assert specific number of elements if (is_numeric($count)) { $counted = $tags ? count($tags) : 0; self::assertEquals($count, $counted, $message); } // assert any elements exist if true, assert no elements exist if false elseif (is_bool($count)) { $any = count($tags) > 0 && $tags[0] instanceof DOMNode; if ($count) { self::assertTrue($any, $message); } else { self::assertFalse($any, $message); } } // check for range number of elements elseif (is_array($count) && (isset($count['>']) || isset($count['<']) || isset($count['>=']) || isset($count['<=']))) { $counted = $tags ? count($tags) : 0; if (isset($count['>'])) { self::assertTrue($counted > $count['>'], $message); } if (isset($count['>='])) { self::assertTrue($counted >= $count['>='], $message); } if (isset($count['<'])) { self::assertTrue($counted < $count['<'], $message); } if (isset($count['<='])) { self::assertTrue($counted <= $count['<='], $message); } } else { throw new PHPUnit_Framework_Exception; } } /** * Evaluate an HTML or XML string and assert its structure and/or contents. * * The first argument ($matcher) is an associative array that specifies the * match criteria for the assertion: * * - `id` : the node with the given id attribute must match the * corresponding value. * - `tag` : the node type must match the corresponding value. * - `attributes` : a hash. The node's attributes must match the * corresponding values in the hash. * - `content` : The text content must match the given value. * - `parent` : a hash. The node's parent must match the * corresponding hash. * - `child` : a hash. At least one of the node's immediate children * must meet the criteria described by the hash. * - `ancestor` : a hash. At least one of the node's ancestors must * meet the criteria described by the hash. * - `descendant` : a hash. At least one of the node's descendants must * meet the criteria described by the hash. * - `children` : a hash, for counting children of a node. * Accepts the keys: * - `count` : a number which must equal the number of children * that match * - `less_than` : the number of matching children must be greater * than this number * - `greater_than` : the number of matching children must be less than * this number * - `only` : another hash consisting of the keys to use to match * on the children, and only matching children will be * counted * * * // Matcher that asserts that there is an element with an id="my_id". * $matcher = array('id' => 'my_id'); * * // Matcher that asserts that there is a "span" tag. * $matcher = array('tag' => 'span'); * * // Matcher that asserts that there is a "span" tag with the content * // "Hello World". * $matcher = array('tag' => 'span', 'content' => 'Hello World'); * * // Matcher that asserts that there is a "span" tag with content matching * // the regular expression pattern. * $matcher = array('tag' => 'span', 'content' => 'regexp:/Try P(HP|ython)/'); * * // Matcher that asserts that there is a "span" with an "list" class * // attribute. * $matcher = array( * 'tag' => 'span', * 'attributes' => array('class' => 'list') * ); * * // Matcher that asserts that there is a "span" inside of a "div". * $matcher = array( * 'tag' => 'span', * 'parent' => array('tag' => 'div') * ); * * // Matcher that asserts that there is a "span" somewhere inside a * // "table". * $matcher = array( * 'tag' => 'span', * 'ancestor' => array('tag' => 'table') * ); * * // Matcher that asserts that there is a "span" with at least one "em" * // child. * $matcher = array( * 'tag' => 'span', * 'child' => array('tag' => 'em') * ); * * // Matcher that asserts that there is a "span" containing a (possibly * // nested) "strong" tag. * $matcher = array( * 'tag' => 'span', * 'descendant' => array('tag' => 'strong') * ); * * // Matcher that asserts that there is a "span" containing 5-10 "em" tags * // as immediate children. * $matcher = array( * 'tag' => 'span', * 'children' => array( * 'less_than' => 11, * 'greater_than' => 4, * 'only' => array('tag' => 'em') * ) * ); * * // Matcher that asserts that there is a "div", with an "ul" ancestor and * // a "li" parent (with class="enum"), and containing a "span" descendant * // that contains an element with id="my_test" and the text "Hello World". * $matcher = array( * 'tag' => 'div', * 'ancestor' => array('tag' => 'ul'), * 'parent' => array( * 'tag' => 'li', * 'attributes' => array('class' => 'enum') * ), * 'descendant' => array( * 'tag' => 'span', * 'child' => array( * 'id' => 'my_test', * 'content' => 'Hello World' * ) * ) * ); * * // Use assertTag() to apply a $matcher to a piece of $html. * $this->assertTag($matcher, $html); * * // Use assertTag() to apply a $matcher to a piece of $xml. * $this->assertTag($matcher, $xml, '', false); * * * The second argument ($actual) is a string containing either HTML or * XML text to be tested. * * The third argument ($message) is an optional message that will be * used if the assertion fails. * * The fourth argument ($html) is an optional flag specifying whether * to load the $actual string into a DOMDocument using the HTML or * XML load strategy. It is true by default, which assumes the HTML * load strategy. In many cases, this will be acceptable for XML as well. * * @param array $matcher * @param string $actual * @param string $message * @param bool $isHtml * * @since Method available since Release 3.3.0 * @deprecated * @codeCoverageIgnore */ public static function assertTag($matcher, $actual, $message = '', $isHtml = true) { trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED); $dom = PHPUnit_Util_XML::load($actual, $isHtml); $tags = PHPUnit_Util_XML::findNodes($dom, $matcher, $isHtml); $matched = count($tags) > 0 && $tags[0] instanceof DOMNode; self::assertTrue($matched, $message); } /** * This assertion is the exact opposite of assertTag(). * * Rather than asserting that $matcher results in a match, it asserts that * $matcher does not match. * * @param array $matcher * @param string $actual * @param string $message * @param bool $isHtml * * @since Method available since Release 3.3.0 * @deprecated * @codeCoverageIgnore */ public static function assertNotTag($matcher, $actual, $message = '', $isHtml = true) { trigger_error(__METHOD__ . ' is deprecated', E_USER_DEPRECATED); $dom = PHPUnit_Util_XML::load($actual, $isHtml); $tags = PHPUnit_Util_XML::findNodes($dom, $matcher, $isHtml); $matched = count($tags) > 0 && $tags[0] instanceof DOMNode; self::assertFalse($matched, $message); } /** * Evaluates a PHPUnit_Framework_Constraint matcher object. * * @param mixed $value * @param PHPUnit_Framework_Constraint $constraint * @param string $message * * @since Method available since Release 3.0.0 */ public static function assertThat($value, PHPUnit_Framework_Constraint $constraint, $message = '') { self::$count += count($constraint); $constraint->evaluate($value, $message); } /** * Asserts that a string is a valid JSON string. * * @param string $actualJson * @param string $message * * @since Method available since Release 3.7.20 */ public static function assertJson($actualJson, $message = '') { if (!is_string($actualJson)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } self::assertThat($actualJson, self::isJson(), $message); } /** * Asserts that two given JSON encoded objects or arrays are equal. * * @param string $expectedJson * @param string $actualJson * @param string $message */ public static function assertJsonStringEqualsJsonString($expectedJson, $actualJson, $message = '') { self::assertJson($expectedJson, $message); self::assertJson($actualJson, $message); $expected = json_decode($expectedJson); $actual = json_decode($actualJson); self::assertEquals($expected, $actual, $message); } /** * Asserts that two given JSON encoded objects or arrays are not equal. * * @param string $expectedJson * @param string $actualJson * @param string $message */ public static function assertJsonStringNotEqualsJsonString($expectedJson, $actualJson, $message = '') { self::assertJson($expectedJson, $message); self::assertJson($actualJson, $message); $expected = json_decode($expectedJson); $actual = json_decode($actualJson); self::assertNotEquals($expected, $actual, $message); } /** * Asserts that the generated JSON encoded object and the content of the given file are equal. * * @param string $expectedFile * @param string $actualJson * @param string $message */ public static function assertJsonStringEqualsJsonFile($expectedFile, $actualJson, $message = '') { self::assertFileExists($expectedFile, $message); $expectedJson = file_get_contents($expectedFile); self::assertJson($expectedJson, $message); self::assertJson($actualJson, $message); // call constraint $constraint = new PHPUnit_Framework_Constraint_JsonMatches( $expectedJson ); self::assertThat($actualJson, $constraint, $message); } /** * Asserts that the generated JSON encoded object and the content of the given file are not equal. * * @param string $expectedFile * @param string $actualJson * @param string $message */ public static function assertJsonStringNotEqualsJsonFile($expectedFile, $actualJson, $message = '') { self::assertFileExists($expectedFile, $message); $expectedJson = file_get_contents($expectedFile); self::assertJson($expectedJson, $message); self::assertJson($actualJson, $message); // call constraint $constraint = new PHPUnit_Framework_Constraint_JsonMatches( $expectedJson ); self::assertThat($actualJson, new PHPUnit_Framework_Constraint_Not($constraint), $message); } /** * Asserts that two JSON files are not equal. * * @param string $expectedFile * @param string $actualFile * @param string $message */ public static function assertJsonFileNotEqualsJsonFile($expectedFile, $actualFile, $message = '') { self::assertFileExists($expectedFile, $message); self::assertFileExists($actualFile, $message); $actualJson = file_get_contents($actualFile); $expectedJson = file_get_contents($expectedFile); self::assertJson($expectedJson, $message); self::assertJson($actualJson, $message); // call constraint $constraintExpected = new PHPUnit_Framework_Constraint_JsonMatches( $expectedJson ); $constraintActual = new PHPUnit_Framework_Constraint_JsonMatches($actualJson); self::assertThat($expectedJson, new PHPUnit_Framework_Constraint_Not($constraintActual), $message); self::assertThat($actualJson, new PHPUnit_Framework_Constraint_Not($constraintExpected), $message); } /** * Asserts that two JSON files are equal. * * @param string $expectedFile * @param string $actualFile * @param string $message */ public static function assertJsonFileEqualsJsonFile($expectedFile, $actualFile, $message = '') { self::assertFileExists($expectedFile, $message); self::assertFileExists($actualFile, $message); $actualJson = file_get_contents($actualFile); $expectedJson = file_get_contents($expectedFile); self::assertJson($expectedJson, $message); self::assertJson($actualJson, $message); // call constraint $constraintExpected = new PHPUnit_Framework_Constraint_JsonMatches( $expectedJson ); $constraintActual = new PHPUnit_Framework_Constraint_JsonMatches($actualJson); self::assertThat($expectedJson, $constraintActual, $message); self::assertThat($actualJson, $constraintExpected, $message); } /** * Returns a PHPUnit_Framework_Constraint_And matcher object. * * @return PHPUnit_Framework_Constraint_And * * @since Method available since Release 3.0.0 */ public static function logicalAnd() { $constraints = func_get_args(); $constraint = new PHPUnit_Framework_Constraint_And; $constraint->setConstraints($constraints); return $constraint; } /** * Returns a PHPUnit_Framework_Constraint_Or matcher object. * * @return PHPUnit_Framework_Constraint_Or * * @since Method available since Release 3.0.0 */ public static function logicalOr() { $constraints = func_get_args(); $constraint = new PHPUnit_Framework_Constraint_Or; $constraint->setConstraints($constraints); return $constraint; } /** * Returns a PHPUnit_Framework_Constraint_Not matcher object. * * @param PHPUnit_Framework_Constraint $constraint * * @return PHPUnit_Framework_Constraint_Not * * @since Method available since Release 3.0.0 */ public static function logicalNot(PHPUnit_Framework_Constraint $constraint) { return new PHPUnit_Framework_Constraint_Not($constraint); } /** * Returns a PHPUnit_Framework_Constraint_Xor matcher object. * * @return PHPUnit_Framework_Constraint_Xor * * @since Method available since Release 3.0.0 */ public static function logicalXor() { $constraints = func_get_args(); $constraint = new PHPUnit_Framework_Constraint_Xor; $constraint->setConstraints($constraints); return $constraint; } /** * Returns a PHPUnit_Framework_Constraint_IsAnything matcher object. * * @return PHPUnit_Framework_Constraint_IsAnything * * @since Method available since Release 3.0.0 */ public static function anything() { return new PHPUnit_Framework_Constraint_IsAnything; } /** * Returns a PHPUnit_Framework_Constraint_IsTrue matcher object. * * @return PHPUnit_Framework_Constraint_IsTrue * * @since Method available since Release 3.3.0 */ public static function isTrue() { return new PHPUnit_Framework_Constraint_IsTrue; } /** * Returns a PHPUnit_Framework_Constraint_Callback matcher object. * * @param callable $callback * * @return PHPUnit_Framework_Constraint_Callback */ public static function callback($callback) { return new PHPUnit_Framework_Constraint_Callback($callback); } /** * Returns a PHPUnit_Framework_Constraint_IsFalse matcher object. * * @return PHPUnit_Framework_Constraint_IsFalse * * @since Method available since Release 3.3.0 */ public static function isFalse() { return new PHPUnit_Framework_Constraint_IsFalse; } /** * Returns a PHPUnit_Framework_Constraint_IsJson matcher object. * * @return PHPUnit_Framework_Constraint_IsJson * * @since Method available since Release 3.7.20 */ public static function isJson() { return new PHPUnit_Framework_Constraint_IsJson; } /** * Returns a PHPUnit_Framework_Constraint_IsNull matcher object. * * @return PHPUnit_Framework_Constraint_IsNull * * @since Method available since Release 3.3.0 */ public static function isNull() { return new PHPUnit_Framework_Constraint_IsNull; } /** * Returns a PHPUnit_Framework_Constraint_Attribute matcher object. * * @param PHPUnit_Framework_Constraint $constraint * @param string $attributeName * * @return PHPUnit_Framework_Constraint_Attribute * * @since Method available since Release 3.1.0 */ public static function attribute(PHPUnit_Framework_Constraint $constraint, $attributeName) { return new PHPUnit_Framework_Constraint_Attribute( $constraint, $attributeName ); } /** * Returns a PHPUnit_Framework_Constraint_TraversableContains matcher * object. * * @param mixed $value * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @return PHPUnit_Framework_Constraint_TraversableContains * * @since Method available since Release 3.0.0 */ public static function contains($value, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { return new PHPUnit_Framework_Constraint_TraversableContains($value, $checkForObjectIdentity, $checkForNonObjectIdentity); } /** * Returns a PHPUnit_Framework_Constraint_TraversableContainsOnly matcher * object. * * @param string $type * * @return PHPUnit_Framework_Constraint_TraversableContainsOnly * * @since Method available since Release 3.1.4 */ public static function containsOnly($type) { return new PHPUnit_Framework_Constraint_TraversableContainsOnly($type); } /** * Returns a PHPUnit_Framework_Constraint_TraversableContainsOnly matcher * object. * * @param string $classname * * @return PHPUnit_Framework_Constraint_TraversableContainsOnly */ public static function containsOnlyInstancesOf($classname) { return new PHPUnit_Framework_Constraint_TraversableContainsOnly($classname, false); } /** * Returns a PHPUnit_Framework_Constraint_ArrayHasKey matcher object. * * @param mixed $key * * @return PHPUnit_Framework_Constraint_ArrayHasKey * * @since Method available since Release 3.0.0 */ public static function arrayHasKey($key) { return new PHPUnit_Framework_Constraint_ArrayHasKey($key); } /** * Returns a PHPUnit_Framework_Constraint_IsEqual matcher object. * * @param mixed $value * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase * * @return PHPUnit_Framework_Constraint_IsEqual * * @since Method available since Release 3.0.0 */ public static function equalTo($value, $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { return new PHPUnit_Framework_Constraint_IsEqual( $value, $delta, $maxDepth, $canonicalize, $ignoreCase ); } /** * Returns a PHPUnit_Framework_Constraint_IsEqual matcher object * that is wrapped in a PHPUnit_Framework_Constraint_Attribute matcher * object. * * @param string $attributeName * @param mixed $value * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase * * @return PHPUnit_Framework_Constraint_Attribute * * @since Method available since Release 3.1.0 */ public static function attributeEqualTo($attributeName, $value, $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { return self::attribute( self::equalTo( $value, $delta, $maxDepth, $canonicalize, $ignoreCase ), $attributeName ); } /** * Returns a PHPUnit_Framework_Constraint_IsEmpty matcher object. * * @return PHPUnit_Framework_Constraint_IsEmpty * * @since Method available since Release 3.5.0 */ public static function isEmpty() { return new PHPUnit_Framework_Constraint_IsEmpty; } /** * Returns a PHPUnit_Framework_Constraint_FileExists matcher object. * * @return PHPUnit_Framework_Constraint_FileExists * * @since Method available since Release 3.0.0 */ public static function fileExists() { return new PHPUnit_Framework_Constraint_FileExists; } /** * Returns a PHPUnit_Framework_Constraint_GreaterThan matcher object. * * @param mixed $value * * @return PHPUnit_Framework_Constraint_GreaterThan * * @since Method available since Release 3.0.0 */ public static function greaterThan($value) { return new PHPUnit_Framework_Constraint_GreaterThan($value); } /** * Returns a PHPUnit_Framework_Constraint_Or matcher object that wraps * a PHPUnit_Framework_Constraint_IsEqual and a * PHPUnit_Framework_Constraint_GreaterThan matcher object. * * @param mixed $value * * @return PHPUnit_Framework_Constraint_Or * * @since Method available since Release 3.1.0 */ public static function greaterThanOrEqual($value) { return self::logicalOr( new PHPUnit_Framework_Constraint_IsEqual($value), new PHPUnit_Framework_Constraint_GreaterThan($value) ); } /** * Returns a PHPUnit_Framework_Constraint_ClassHasAttribute matcher object. * * @param string $attributeName * * @return PHPUnit_Framework_Constraint_ClassHasAttribute * * @since Method available since Release 3.1.0 */ public static function classHasAttribute($attributeName) { return new PHPUnit_Framework_Constraint_ClassHasAttribute( $attributeName ); } /** * Returns a PHPUnit_Framework_Constraint_ClassHasStaticAttribute matcher * object. * * @param string $attributeName * * @return PHPUnit_Framework_Constraint_ClassHasStaticAttribute * * @since Method available since Release 3.1.0 */ public static function classHasStaticAttribute($attributeName) { return new PHPUnit_Framework_Constraint_ClassHasStaticAttribute( $attributeName ); } /** * Returns a PHPUnit_Framework_Constraint_ObjectHasAttribute matcher object. * * @param string $attributeName * * @return PHPUnit_Framework_Constraint_ObjectHasAttribute * * @since Method available since Release 3.0.0 */ public static function objectHasAttribute($attributeName) { return new PHPUnit_Framework_Constraint_ObjectHasAttribute( $attributeName ); } /** * Returns a PHPUnit_Framework_Constraint_IsIdentical matcher object. * * @param mixed $value * * @return PHPUnit_Framework_Constraint_IsIdentical * * @since Method available since Release 3.0.0 */ public static function identicalTo($value) { return new PHPUnit_Framework_Constraint_IsIdentical($value); } /** * Returns a PHPUnit_Framework_Constraint_IsInstanceOf matcher object. * * @param string $className * * @return PHPUnit_Framework_Constraint_IsInstanceOf * * @since Method available since Release 3.0.0 */ public static function isInstanceOf($className) { return new PHPUnit_Framework_Constraint_IsInstanceOf($className); } /** * Returns a PHPUnit_Framework_Constraint_IsType matcher object. * * @param string $type * * @return PHPUnit_Framework_Constraint_IsType * * @since Method available since Release 3.0.0 */ public static function isType($type) { return new PHPUnit_Framework_Constraint_IsType($type); } /** * Returns a PHPUnit_Framework_Constraint_LessThan matcher object. * * @param mixed $value * * @return PHPUnit_Framework_Constraint_LessThan * * @since Method available since Release 3.0.0 */ public static function lessThan($value) { return new PHPUnit_Framework_Constraint_LessThan($value); } /** * Returns a PHPUnit_Framework_Constraint_Or matcher object that wraps * a PHPUnit_Framework_Constraint_IsEqual and a * PHPUnit_Framework_Constraint_LessThan matcher object. * * @param mixed $value * * @return PHPUnit_Framework_Constraint_Or * * @since Method available since Release 3.1.0 */ public static function lessThanOrEqual($value) { return self::logicalOr( new PHPUnit_Framework_Constraint_IsEqual($value), new PHPUnit_Framework_Constraint_LessThan($value) ); } /** * Returns a PHPUnit_Framework_Constraint_PCREMatch matcher object. * * @param string $pattern * * @return PHPUnit_Framework_Constraint_PCREMatch * * @since Method available since Release 3.0.0 */ public static function matchesRegularExpression($pattern) { return new PHPUnit_Framework_Constraint_PCREMatch($pattern); } /** * Returns a PHPUnit_Framework_Constraint_StringMatches matcher object. * * @param string $string * * @return PHPUnit_Framework_Constraint_StringMatches * * @since Method available since Release 3.5.0 */ public static function matches($string) { return new PHPUnit_Framework_Constraint_StringMatches($string); } /** * Returns a PHPUnit_Framework_Constraint_StringStartsWith matcher object. * * @param mixed $prefix * * @return PHPUnit_Framework_Constraint_StringStartsWith * * @since Method available since Release 3.4.0 */ public static function stringStartsWith($prefix) { return new PHPUnit_Framework_Constraint_StringStartsWith($prefix); } /** * Returns a PHPUnit_Framework_Constraint_StringContains matcher object. * * @param string $string * @param bool $case * * @return PHPUnit_Framework_Constraint_StringContains * * @since Method available since Release 3.0.0 */ public static function stringContains($string, $case = true) { return new PHPUnit_Framework_Constraint_StringContains($string, $case); } /** * Returns a PHPUnit_Framework_Constraint_StringEndsWith matcher object. * * @param mixed $suffix * * @return PHPUnit_Framework_Constraint_StringEndsWith * * @since Method available since Release 3.4.0 */ public static function stringEndsWith($suffix) { return new PHPUnit_Framework_Constraint_StringEndsWith($suffix); } /** * Returns a PHPUnit_Framework_Constraint_Count matcher object. * * @param int $count * * @return PHPUnit_Framework_Constraint_Count */ public static function countOf($count) { return new PHPUnit_Framework_Constraint_Count($count); } /** * Fails a test with the given message. * * @param string $message * * @throws PHPUnit_Framework_AssertionFailedError */ public static function fail($message = '') { throw new PHPUnit_Framework_AssertionFailedError($message); } /** * Returns the value of an attribute of a class or an object. * This also works for attributes that are declared protected or private. * * @param mixed $classOrObject * @param string $attributeName * * @return mixed * * @throws PHPUnit_Framework_Exception */ public static function readAttribute($classOrObject, $attributeName) { if (!is_string($attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } if (!preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'valid attribute name'); } if (is_string($classOrObject)) { if (!class_exists($classOrObject)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 1, 'class name' ); } return self::getStaticAttribute( $classOrObject, $attributeName ); } elseif (is_object($classOrObject)) { return self::getObjectAttribute( $classOrObject, $attributeName ); } else { throw PHPUnit_Util_InvalidArgumentHelper::factory( 1, 'class name or object' ); } } /** * Returns the value of a static attribute. * This also works for attributes that are declared protected or private. * * @param string $className * @param string $attributeName * * @return mixed * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 4.0.0 */ public static function getStaticAttribute($className, $attributeName) { if (!is_string($className)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!class_exists($className)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'class name'); } if (!is_string($attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } if (!preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'valid attribute name'); } $class = new ReflectionClass($className); while ($class) { $attributes = $class->getStaticProperties(); if (array_key_exists($attributeName, $attributes)) { return $attributes[$attributeName]; } $class = $class->getParentClass(); } throw new PHPUnit_Framework_Exception( sprintf( 'Attribute "%s" not found in class.', $attributeName ) ); } /** * Returns the value of an object's attribute. * This also works for attributes that are declared protected or private. * * @param object $object * @param string $attributeName * * @return mixed * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 4.0.0 */ public static function getObjectAttribute($object, $attributeName) { if (!is_object($object)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'object'); } if (!is_string($attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'string'); } if (!preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $attributeName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'valid attribute name'); } try { $attribute = new ReflectionProperty($object, $attributeName); } catch (ReflectionException $e) { $reflector = new ReflectionObject($object); while ($reflector = $reflector->getParentClass()) { try { $attribute = $reflector->getProperty($attributeName); break; } catch (ReflectionException $e) { } } } if (isset($attribute)) { if (!$attribute || $attribute->isPublic()) { return $object->$attributeName; } $attribute->setAccessible(true); $value = $attribute->getValue($object); $attribute->setAccessible(false); return $value; } throw new PHPUnit_Framework_Exception( sprintf( 'Attribute "%s" not found in object.', $attributeName ) ); } /** * Mark the test as incomplete. * * @param string $message * * @throws PHPUnit_Framework_IncompleteTestError * * @since Method available since Release 3.0.0 */ public static function markTestIncomplete($message = '') { throw new PHPUnit_Framework_IncompleteTestError($message); } /** * Mark the test as skipped. * * @param string $message * * @throws PHPUnit_Framework_SkippedTestError * * @since Method available since Release 3.0.0 */ public static function markTestSkipped($message = '') { throw new PHPUnit_Framework_SkippedTestError($message); } /** * Return the current assertion count. * * @return int * * @since Method available since Release 3.3.3 */ public static function getCount() { return self::$count; } /** * Reset the assertion counter. * * @since Method available since Release 3.3.3 */ public static function resetCount() { self::$count = 0; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Thrown when an assertion failed. * * @since Class available since Release 2.0.0 */ class PHPUnit_Framework_AssertionFailedError extends PHPUnit_Framework_Exception implements PHPUnit_Framework_SelfDescribing { /** * Wrapper for getMessage() which is declared as final. * * @return string */ public function toString() { return $this->getMessage(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * An empty Listener that can be extended to implement TestListener * with just a few lines of code. * * @see PHPUnit_Framework_TestListener for documentation on the API methods. * @since Class available since Release 4.0.0 */ abstract class PHPUnit_Framework_BaseTestListener implements PHPUnit_Framework_TestListener { public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { } public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { } public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { } public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { } public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { } public function startTestSuite(PHPUnit_Framework_TestSuite $suite) { } public function endTestSuite(PHPUnit_Framework_TestSuite $suite) { } public function startTest(PHPUnit_Framework_Test $test) { } public function endTest(PHPUnit_Framework_Test $test, $time) { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 4.0.0 */ class PHPUnit_Framework_CodeCoverageException extends PHPUnit_Framework_Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Logical AND. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_And extends PHPUnit_Framework_Constraint { /** * @var PHPUnit_Framework_Constraint[] */ protected $constraints = array(); /** * @var PHPUnit_Framework_Constraint */ protected $lastConstraint = null; /** * @param PHPUnit_Framework_Constraint[] $constraints * * @throws PHPUnit_Framework_Exception */ public function setConstraints(array $constraints) { $this->constraints = array(); foreach ($constraints as $constraint) { if (!($constraint instanceof PHPUnit_Framework_Constraint)) { throw new PHPUnit_Framework_Exception( 'All parameters to ' . __CLASS__ . ' must be a constraint object.' ); } $this->constraints[] = $constraint; } } /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { $success = true; $constraint = null; foreach ($this->constraints as $constraint) { if (!$constraint->evaluate($other, $description, true)) { $success = false; break; } } if ($returnResult) { return $success; } if (!$success) { $this->fail($other, $description); } } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { $text = ''; foreach ($this->constraints as $key => $constraint) { if ($key > 0) { $text .= ' and '; } $text .= $constraint->toString(); } return $text; } /** * Counts the number of constraint elements. * * @return int * * @since Method available since Release 3.4.0 */ public function count() { $count = 0; foreach ($this->constraints as $constraint) { $count += count($constraint); } return $count; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the array it is evaluated for has a given key. * * Uses array_key_exists() to check if the key is found in the input array, if * not found the evaluation fails. * * The array key is passed in the constructor. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_ArrayHasKey extends PHPUnit_Framework_Constraint { /** * @var int|string */ protected $key; /** * @param int|string $key */ public function __construct($key) { parent::__construct(); $this->key = $key; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { if (is_array($other)) { return array_key_exists($this->key, $other); } if ($other instanceof ArrayAccess) { return $other->offsetExists($this->key); } return false; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'has the key ' . $this->exporter->export($this->key); } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return 'an array ' . $this->toString(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the array it is evaluated for has a specified subset. * * Uses array_replace_recursive() to check if a key value subset is part of the * subject array. * * @since Class available since Release 4.4.0 */ class PHPUnit_Framework_Constraint_ArraySubset extends PHPUnit_Framework_Constraint { /** * @var array|ArrayAccess */ protected $subset; /** * @var bool */ protected $strict; /** * @param array|ArrayAccess $subset * @param bool $strict Check for object identity */ public function __construct($subset, $strict = false) { parent::__construct(); $this->strict = $strict; $this->subset = $subset; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param array|ArrayAccess $other Array or ArrayAccess object to evaluate. * * @return bool */ protected function matches($other) { $patched = array_replace_recursive($other, $this->subset); if ($this->strict) { return $other === $patched; } else { return $other == $patched; } } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'has the subset ' . $this->exporter->export($this->subset); } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return 'an array ' . $this->toString(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 3.1.0 */ class PHPUnit_Framework_Constraint_Attribute extends PHPUnit_Framework_Constraint_Composite { /** * @var string */ protected $attributeName; /** * @param PHPUnit_Framework_Constraint $constraint * @param string $attributeName */ public function __construct(PHPUnit_Framework_Constraint $constraint, $attributeName) { parent::__construct($constraint); $this->attributeName = $attributeName; } /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { return parent::evaluate( PHPUnit_Framework_Assert::readAttribute( $other, $this->attributeName ), $description, $returnResult ); } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'attribute "' . $this->attributeName . '" ' . $this->innerConstraint->toString(); } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return $this->toString(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that evaluates against a specified closure. */ class PHPUnit_Framework_Constraint_Callback extends PHPUnit_Framework_Constraint { private $callback; /** * @param callable $callback * * @throws PHPUnit_Framework_Exception */ public function __construct($callback) { if (!is_callable($callback)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 1, 'callable' ); } parent::__construct(); $this->callback = $callback; } /** * Evaluates the constraint for parameter $value. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return call_user_func($this->callback, $other); } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'is accepted by specified callback'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the class it is evaluated for has a given * attribute. * * The attribute name is passed in the constructor. * * @since Class available since Release 3.1.0 */ class PHPUnit_Framework_Constraint_ClassHasAttribute extends PHPUnit_Framework_Constraint { /** * @var string */ protected $attributeName; /** * @param string $attributeName */ public function __construct($attributeName) { parent::__construct(); $this->attributeName = $attributeName; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { $class = new ReflectionClass($other); return $class->hasProperty($this->attributeName); } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return sprintf( 'has attribute "%s"', $this->attributeName ); } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return sprintf( '%sclass "%s" %s', is_object($other) ? 'object of ' : '', is_object($other) ? get_class($other) : $other, $this->toString() ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the class it is evaluated for has a given * static attribute. * * The attribute name is passed in the constructor. * * @since Class available since Release 3.1.0 */ class PHPUnit_Framework_Constraint_ClassHasStaticAttribute extends PHPUnit_Framework_Constraint_ClassHasAttribute { /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { $class = new ReflectionClass($other); if ($class->hasProperty($this->attributeName)) { $attribute = $class->getProperty($this->attributeName); return $attribute->isStatic(); } else { return false; } } /** * Returns a string representation of the constraint. * * @return string * * @since Method available since Release 3.3.0 */ public function toString() { return sprintf( 'has static attribute "%s"', $this->attributeName ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 3.1.0 */ abstract class PHPUnit_Framework_Constraint_Composite extends PHPUnit_Framework_Constraint { /** * @var PHPUnit_Framework_Constraint */ protected $innerConstraint; /** * @param PHPUnit_Framework_Constraint $innerConstraint */ public function __construct(PHPUnit_Framework_Constraint $innerConstraint) { parent::__construct(); $this->innerConstraint = $innerConstraint; } /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { try { return $this->innerConstraint->evaluate( $other, $description, $returnResult ); } catch (PHPUnit_Framework_ExpectationFailedException $e) { $this->fail($other, $description); } } /** * Counts the number of constraint elements. * * @return int */ public function count() { return count($this->innerConstraint); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 3.6.0 */ class PHPUnit_Framework_Constraint_Count extends PHPUnit_Framework_Constraint { /** * @var int */ protected $expectedCount = 0; /** * @param int $expected */ public function __construct($expected) { parent::__construct(); $this->expectedCount = $expected; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other * * @return bool */ protected function matches($other) { return $this->expectedCount === $this->getCountOf($other); } /** * @param mixed $other * * @return bool */ protected function getCountOf($other) { if ($other instanceof Countable || is_array($other)) { return count($other); } elseif ($other instanceof Traversable) { if ($other instanceof IteratorAggregate) { $iterator = $other->getIterator(); } else { $iterator = $other; } $key = $iterator->key(); $count = iterator_count($iterator); // manually rewind $iterator to previous key, since iterator_count // moves pointer if ($key !== null) { $iterator->rewind(); while ($iterator->valid() && $key !== $iterator->key()) { $iterator->next(); } } return $count; } } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return sprintf( 'actual size %d matches expected size %d', $this->getCountOf($other), $this->expectedCount ); } /** * @return string */ public function toString() { return sprintf( 'count matches %d', $this->expectedCount ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 3.6.6 */ class PHPUnit_Framework_Constraint_Exception extends PHPUnit_Framework_Constraint { /** * @var string */ protected $className; /** * @param string $className */ public function __construct($className) { parent::__construct(); $this->className = $className; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return $other instanceof $this->className; } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { if ($other !== null) { $message = ''; if ($other instanceof Exception) { $message = '. Message was: "' . $other->getMessage() . '" at' . "\n" . $other->getTraceAsString(); } return sprintf( 'exception of type "%s" matches expected exception "%s"%s', get_class($other), $this->className, $message ); } return sprintf( 'exception of type "%s" is thrown', $this->className ); } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return sprintf( 'exception of type "%s"', $this->className ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 3.6.6 */ class PHPUnit_Framework_Constraint_ExceptionCode extends PHPUnit_Framework_Constraint { /** * @var int */ protected $expectedCode; /** * @param int $expected */ public function __construct($expected) { parent::__construct(); $this->expectedCode = $expected; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param Exception $other * * @return bool */ protected function matches($other) { return (string) $other->getCode() == (string) $this->expectedCode; } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return sprintf( '%s is equal to expected exception code %s', $this->exporter->export($other->getCode()), $this->exporter->export($this->expectedCode) ); } /** * @return string */ public function toString() { return 'exception code is '; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 3.6.6 */ class PHPUnit_Framework_Constraint_ExceptionMessage extends PHPUnit_Framework_Constraint { /** * @var int */ protected $expectedMessage; /** * @param string $expected */ public function __construct($expected) { parent::__construct(); $this->expectedMessage = $expected; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param Exception $other * * @return bool */ protected function matches($other) { return strpos($other->getMessage(), $this->expectedMessage) !== false; } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return sprintf( "exception message '%s' contains '%s'", $other->getMessage(), $this->expectedMessage ); } /** * @return string */ public function toString() { return 'exception message contains '; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 4.3.0 */ class PHPUnit_Framework_Constraint_ExceptionMessageRegExp extends PHPUnit_Framework_Constraint { /** * @var int */ protected $expectedMessageRegExp; /** * @param string $expected */ public function __construct($expected) { parent::__construct(); $this->expectedMessageRegExp = $expected; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param Exception $other * * @return bool */ protected function matches($other) { $match = PHPUnit_Util_Regex::pregMatchSafe($this->expectedMessageRegExp, $other->getMessage()); if (false === $match) { throw new PHPUnit_Framework_Exception( "Invalid expected exception message regex given: '{$this->expectedMessageRegExp}'" ); } return 1 === $match; } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return sprintf( "exception message '%s' matches '%s'", $other->getMessage(), $this->expectedMessageRegExp ); } /** * @return string */ public function toString() { return 'exception message matches '; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that checks if the file(name) that it is evaluated for exists. * * The file path to check is passed as $other in evaluate(). * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_FileExists extends PHPUnit_Framework_Constraint { /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return file_exists($other); } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return sprintf( 'file "%s" exists', $other ); } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'file exists'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the value it is evaluated for is greater * than a given value. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_GreaterThan extends PHPUnit_Framework_Constraint { /** * @var numeric */ protected $value; /** * @param numeric $value */ public function __construct($value) { parent::__construct(); $this->value = $value; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return $this->value < $other; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'is greater than ' . $this->exporter->export($this->value); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that accepts any input value. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_IsAnything extends PHPUnit_Framework_Constraint { /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { return $returnResult ? true : null; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'is anything'; } /** * Counts the number of constraint elements. * * @return int * * @since Method available since Release 3.5.0 */ public function count() { return 0; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that checks whether a variable is empty(). * * @since Class available since Release 3.5.0 */ class PHPUnit_Framework_Constraint_IsEmpty extends PHPUnit_Framework_Constraint { /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { if ($other instanceof Countable) { return count($other) === 0; } return empty($other); } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'is empty'; } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { $type = gettype($other); return sprintf( '%s %s %s', $type[0] == 'a' || $type[0] == 'o' ? 'an' : 'a', $type, $this->toString() ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that checks if one value is equal to another. * * Equality is checked with PHP's == operator, the operator is explained in * detail at {@url http://www.php.net/manual/en/types.comparisons.php}. * Two values are equal if they have the same value disregarding type. * * The expected value is passed in the constructor. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_IsEqual extends PHPUnit_Framework_Constraint { /** * @var mixed */ protected $value; /** * @var float */ protected $delta = 0.0; /** * @var int */ protected $maxDepth = 10; /** * @var bool */ protected $canonicalize = false; /** * @var bool */ protected $ignoreCase = false; /** * @var SebastianBergmann\Comparator\ComparisonFailure */ protected $lastFailure; /** * @param mixed $value * @param float $delta * @param int $maxDepth * @param bool $canonicalize * @param bool $ignoreCase * * @throws PHPUnit_Framework_Exception */ public function __construct($value, $delta = 0.0, $maxDepth = 10, $canonicalize = false, $ignoreCase = false) { parent::__construct(); if (!is_numeric($delta)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'numeric'); } if (!is_int($maxDepth)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(3, 'integer'); } if (!is_bool($canonicalize)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(4, 'boolean'); } if (!is_bool($ignoreCase)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(5, 'boolean'); } $this->value = $value; $this->delta = $delta; $this->maxDepth = $maxDepth; $this->canonicalize = $canonicalize; $this->ignoreCase = $ignoreCase; } /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { // If $this->value and $other are identical, they are also equal. // This is the most common path and will allow us to skip // initialization of all the comparators. if ($this->value === $other) { return true; } $comparatorFactory = SebastianBergmann\Comparator\Factory::getInstance(); try { $comparator = $comparatorFactory->getComparatorFor( $this->value, $other ); $comparator->assertEquals( $this->value, $other, $this->delta, $this->canonicalize, $this->ignoreCase ); } catch (SebastianBergmann\Comparator\ComparisonFailure $f) { if ($returnResult) { return false; } throw new PHPUnit_Framework_ExpectationFailedException( trim($description . "\n" . $f->getMessage()), $f ); } return true; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { $delta = ''; if (is_string($this->value)) { if (strpos($this->value, "\n") !== false) { return 'is equal to '; } else { return sprintf( 'is equal to ', $this->value ); } } else { if ($this->delta != 0) { $delta = sprintf( ' with delta <%F>', $this->delta ); } return sprintf( 'is equal to %s%s', $this->exporter->export($this->value), $delta ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that accepts false. * * @since Class available since Release 3.3.0 */ class PHPUnit_Framework_Constraint_IsFalse extends PHPUnit_Framework_Constraint { /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return $other === false; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'is false'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that one value is identical to another. * * Identical check is performed with PHP's === operator, the operator is * explained in detail at * {@url http://www.php.net/manual/en/types.comparisons.php}. * Two values are identical if they have the same value and are of the same * type. * * The expected value is passed in the constructor. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_IsIdentical extends PHPUnit_Framework_Constraint { /** * @var float */ const EPSILON = 0.0000000001; /** * @var mixed */ protected $value; /** * @param mixed $value */ public function __construct($value) { parent::__construct(); $this->value = $value; } /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { if (is_double($this->value) && is_double($other) && !is_infinite($this->value) && !is_infinite($other) && !is_nan($this->value) && !is_nan($other)) { $success = abs($this->value - $other) < self::EPSILON; } else { $success = $this->value === $other; } if ($returnResult) { return $success; } if (!$success) { $f = null; // if both values are strings, make sure a diff is generated if (is_string($this->value) && is_string($other)) { $f = new SebastianBergmann\Comparator\ComparisonFailure( $this->value, $other, $this->value, $other ); } $this->fail($other, $description, $f); } } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { if (is_object($this->value) && is_object($other)) { return 'two variables reference the same object'; } if (is_string($this->value) && is_string($other)) { return 'two strings are identical'; } return parent::failureDescription($other); } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { if (is_object($this->value)) { return 'is identical to an object of class "' . get_class($this->value) . '"'; } else { return 'is identical to ' . $this->exporter->export($this->value); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the object it is evaluated for is an instance * of a given class. * * The expected class name is passed in the constructor. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_IsInstanceOf extends PHPUnit_Framework_Constraint { /** * @var string */ protected $className; /** * @param string $className */ public function __construct($className) { parent::__construct(); $this->className = $className; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return ($other instanceof $this->className); } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return sprintf( '%s is an instance of %s "%s"', $this->exporter->shortenedExport($other), $this->getType(), $this->className ); } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return sprintf( 'is instance of %s "%s"', $this->getType(), $this->className ); } private function getType() { try { $reflection = new ReflectionClass($this->className); if ($reflection->isInterface()) { return 'interface'; } } catch (ReflectionException $e) { } return 'class'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that a string is valid JSON. * * @since Class available since Release 3.7.20 */ class PHPUnit_Framework_Constraint_IsJson extends PHPUnit_Framework_Constraint { /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { json_decode($other); if (json_last_error()) { return false; } return true; } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { json_decode($other); $error = PHPUnit_Framework_Constraint_JsonMatches_ErrorMessageProvider::determineJsonError( json_last_error() ); return sprintf( '%s is valid JSON (%s)', $this->exporter->shortenedExport($other), $error ); } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'is valid JSON'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that accepts null. * * @since Class available since Release 3.3.0 */ class PHPUnit_Framework_Constraint_IsNull extends PHPUnit_Framework_Constraint { /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return $other === null; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'is null'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that accepts true. * * @since Class available since Release 3.3.0 */ class PHPUnit_Framework_Constraint_IsTrue extends PHPUnit_Framework_Constraint { /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return $other === true; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'is true'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the value it is evaluated for is of a * specified type. * * The expected value is passed in the constructor. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_IsType extends PHPUnit_Framework_Constraint { const TYPE_ARRAY = 'array'; const TYPE_BOOL = 'bool'; const TYPE_FLOAT = 'float'; const TYPE_INT = 'int'; const TYPE_NULL = 'null'; const TYPE_NUMERIC = 'numeric'; const TYPE_OBJECT = 'object'; const TYPE_RESOURCE = 'resource'; const TYPE_STRING = 'string'; const TYPE_SCALAR = 'scalar'; const TYPE_CALLABLE = 'callable'; /** * @var array */ protected $types = array( 'array' => true, 'boolean' => true, 'bool' => true, 'double' => true, 'float' => true, 'integer' => true, 'int' => true, 'null' => true, 'numeric' => true, 'object' => true, 'real' => true, 'resource' => true, 'string' => true, 'scalar' => true, 'callable' => true ); /** * @var string */ protected $type; /** * @param string $type * * @throws PHPUnit_Framework_Exception */ public function __construct($type) { parent::__construct(); if (!isset($this->types[$type])) { throw new PHPUnit_Framework_Exception( sprintf( 'Type specified for PHPUnit_Framework_Constraint_IsType <%s> ' . 'is not a valid type.', $type ) ); } $this->type = $type; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { switch ($this->type) { case 'numeric': return is_numeric($other); case 'integer': case 'int': return is_integer($other); case 'double': case 'float': case 'real': return is_float($other); case 'string': return is_string($other); case 'boolean': case 'bool': return is_bool($other); case 'null': return is_null($other); case 'array': return is_array($other); case 'object': return is_object($other); case 'resource': return is_resource($other) || is_string(@get_resource_type($other)); case 'scalar': return is_scalar($other); case 'callable': return is_callable($other); } } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return sprintf( 'is of type "%s"', $this->type ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Provides human readable messages for each JSON error. * * @since Class available since Release 3.7.0 */ class PHPUnit_Framework_Constraint_JsonMatches_ErrorMessageProvider { /** * Translates JSON error to a human readable string. * * @param string $error * @param string $prefix * * @return string */ public static function determineJsonError($error, $prefix = '') { switch ($error) { case JSON_ERROR_NONE: return; case JSON_ERROR_DEPTH: return $prefix . 'Maximum stack depth exceeded'; case JSON_ERROR_STATE_MISMATCH: return $prefix . 'Underflow or the modes mismatch'; case JSON_ERROR_CTRL_CHAR: return $prefix . 'Unexpected control character found'; case JSON_ERROR_SYNTAX: return $prefix . 'Syntax error, malformed JSON'; case JSON_ERROR_UTF8: return $prefix . 'Malformed UTF-8 characters, possibly incorrectly encoded'; default: return $prefix . 'Unknown error'; } } /** * Translates a given type to a human readable message prefix. * * @param string $type * * @return string */ public static function translateTypeToPrefix($type) { switch (strtolower($type)) { case 'expected': $prefix = 'Expected value JSON decode error - '; break; case 'actual': $prefix = 'Actual value JSON decode error - '; break; default: $prefix = ''; break; } return $prefix; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Asserts whether or not two JSON objects are equal. * * @since Class available since Release 3.7.0 */ class PHPUnit_Framework_Constraint_JsonMatches extends PHPUnit_Framework_Constraint { /** * @var string */ protected $value; /** * Creates a new constraint. * * @param string $value */ public function __construct($value) { parent::__construct(); $this->value = $value; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * This method can be overridden to implement the evaluation algorithm. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { $decodedOther = json_decode($other); if (json_last_error()) { return false; } $decodedValue = json_decode($this->value); if (json_last_error()) { return false; } return $decodedOther == $decodedValue; } /** * Returns a string representation of the object. * * @return string */ public function toString() { return sprintf( 'matches JSON string "%s"', $this->value ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the value it is evaluated for is less than * a given value. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_LessThan extends PHPUnit_Framework_Constraint { /** * @var numeric */ protected $value; /** * @param numeric $value */ public function __construct($value) { parent::__construct(); $this->value = $value; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return $this->value > $other; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'is less than ' . $this->exporter->export($this->value); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Logical NOT. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_Not extends PHPUnit_Framework_Constraint { /** * @var PHPUnit_Framework_Constraint */ protected $constraint; /** * @param PHPUnit_Framework_Constraint $constraint */ public function __construct($constraint) { parent::__construct(); if (!($constraint instanceof PHPUnit_Framework_Constraint)) { $constraint = new PHPUnit_Framework_Constraint_IsEqual($constraint); } $this->constraint = $constraint; } /** * @param string $string * * @return string */ public static function negate($string) { return str_replace( array( 'contains ', 'exists', 'has ', 'is ', 'are ', 'matches ', 'starts with ', 'ends with ', 'reference ', 'not not ' ), array( 'does not contain ', 'does not exist', 'does not have ', 'is not ', 'are not ', 'does not match ', 'starts not with ', 'ends not with ', 'don\'t reference ', 'not ' ), $string ); } /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { $success = !$this->constraint->evaluate($other, $description, true); if ($returnResult) { return $success; } if (!$success) { $this->fail($other, $description); } } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { switch (get_class($this->constraint)) { case 'PHPUnit_Framework_Constraint_And': case 'PHPUnit_Framework_Constraint_Not': case 'PHPUnit_Framework_Constraint_Or': return 'not( ' . $this->constraint->failureDescription($other) . ' )'; default: return self::negate( $this->constraint->failureDescription($other) ); } } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { switch (get_class($this->constraint)) { case 'PHPUnit_Framework_Constraint_And': case 'PHPUnit_Framework_Constraint_Not': case 'PHPUnit_Framework_Constraint_Or': return 'not( ' . $this->constraint->toString() . ' )'; default: return self::negate( $this->constraint->toString() ); } } /** * Counts the number of constraint elements. * * @return int * * @since Method available since Release 3.4.0 */ public function count() { return count($this->constraint); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the object it is evaluated for has a given * attribute. * * The attribute name is passed in the constructor. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_ObjectHasAttribute extends PHPUnit_Framework_Constraint_ClassHasAttribute { /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { $object = new ReflectionObject($other); return $object->hasProperty($this->attributeName); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Logical OR. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_Or extends PHPUnit_Framework_Constraint { /** * @var PHPUnit_Framework_Constraint[] */ protected $constraints = array(); /** * @param PHPUnit_Framework_Constraint[] $constraints */ public function setConstraints(array $constraints) { $this->constraints = array(); foreach ($constraints as $constraint) { if (!($constraint instanceof PHPUnit_Framework_Constraint)) { $constraint = new PHPUnit_Framework_Constraint_IsEqual( $constraint ); } $this->constraints[] = $constraint; } } /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { $success = false; $constraint = null; foreach ($this->constraints as $constraint) { if ($constraint->evaluate($other, $description, true)) { $success = true; break; } } if ($returnResult) { return $success; } if (!$success) { $this->fail($other, $description); } } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { $text = ''; foreach ($this->constraints as $key => $constraint) { if ($key > 0) { $text .= ' or '; } $text .= $constraint->toString(); } return $text; } /** * Counts the number of constraint elements. * * @return int * * @since Method available since Release 3.4.0 */ public function count() { $count = 0; foreach ($this->constraints as $constraint) { $count += count($constraint); } return $count; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the string it is evaluated for matches * a regular expression. * * Checks a given value using the Perl Compatible Regular Expression extension * in PHP. The pattern is matched by executing preg_match(). * * The pattern string passed in the constructor. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_PCREMatch extends PHPUnit_Framework_Constraint { /** * @var string */ protected $pattern; /** * @param string $pattern */ public function __construct($pattern) { parent::__construct(); $this->pattern = $pattern; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return preg_match($this->pattern, $other) > 0; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return sprintf( 'matches PCRE pattern "%s"', $this->pattern ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 3.6.0 */ class PHPUnit_Framework_Constraint_SameSize extends PHPUnit_Framework_Constraint_Count { /** * @var int */ protected $expectedCount; /** * @param int $expected */ public function __construct($expected) { parent::__construct($this->getCountOf($expected)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the string it is evaluated for contains * a given string. * * Uses strpos() to find the position of the string in the input, if not found * the evaluation fails. * * The sub-string is passed in the constructor. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_StringContains extends PHPUnit_Framework_Constraint { /** * @var string */ protected $string; /** * @var bool */ protected $ignoreCase; /** * @param string $string * @param bool $ignoreCase */ public function __construct($string, $ignoreCase = false) { parent::__construct(); $this->string = $string; $this->ignoreCase = $ignoreCase; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { if ($this->ignoreCase) { return stripos($other, $this->string) !== false; } else { return strpos($other, $this->string) !== false; } } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { if ($this->ignoreCase) { $string = strtolower($this->string); } else { $string = $this->string; } return sprintf( 'contains "%s"', $string ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the string it is evaluated for ends with a given * suffix. * * @since Class available since Release 3.4.0 */ class PHPUnit_Framework_Constraint_StringEndsWith extends PHPUnit_Framework_Constraint { /** * @var string */ protected $suffix; /** * @param string $suffix */ public function __construct($suffix) { parent::__construct(); $this->suffix = $suffix; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return substr($other, 0 - strlen($this->suffix)) == $this->suffix; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'ends with "' . $this->suffix . '"'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Diff\Differ; /** * ... * * @since Class available since Release 3.5.0 */ class PHPUnit_Framework_Constraint_StringMatches extends PHPUnit_Framework_Constraint_PCREMatch { /** * @var string */ protected $string; /** * @param string $string */ public function __construct($string) { parent::__construct($string); $this->pattern = $this->createPatternFromFormat( preg_replace('/\r\n/', "\n", $string) ); $this->string = $string; } protected function failureDescription($other) { return 'format description matches text'; } protected function additionalFailureDescription($other) { $from = preg_split('(\r\n|\r|\n)', $this->string); $to = preg_split('(\r\n|\r|\n)', $other); foreach ($from as $index => $line) { if (isset($to[$index]) && $line !== $to[$index]) { $line = $this->createPatternFromFormat($line); if (preg_match($line, $to[$index]) > 0) { $from[$index] = $to[$index]; } } } $this->string = implode("\n", $from); $other = implode("\n", $to); $differ = new Differ("--- Expected\n+++ Actual\n"); return $differ->diff($this->string, $other); } protected function createPatternFromFormat($string) { $string = str_replace( array( '%e', '%s', '%S', '%a', '%A', '%w', '%i', '%d', '%x', '%f', '%c' ), array( '\\' . DIRECTORY_SEPARATOR, '[^\r\n]+', '[^\r\n]*', '.+', '.*', '\s*', '[+-]?\d+', '\d+', '[0-9a-fA-F]+', '[+-]?\.?\d+\.?\d*(?:[Ee][+-]?\d+)?', '.' ), preg_quote($string, '/') ); return '/^' . $string . '$/s'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the string it is evaluated for begins with a * given prefix. * * @since Class available since Release 3.4.0 */ class PHPUnit_Framework_Constraint_StringStartsWith extends PHPUnit_Framework_Constraint { /** * @var string */ protected $prefix; /** * @param string $prefix */ public function __construct($prefix) { parent::__construct(); $this->prefix = $prefix; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return strpos($other, $this->prefix) === 0; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'starts with "' . $this->prefix . '"'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the Traversable it is applied to contains * a given value. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_TraversableContains extends PHPUnit_Framework_Constraint { /** * @var bool */ protected $checkForObjectIdentity; /** * @var bool */ protected $checkForNonObjectIdentity; /** * @var mixed */ protected $value; /** * @param mixed $value * @param bool $checkForObjectIdentity * @param bool $checkForNonObjectIdentity * * @throws PHPUnit_Framework_Exception */ public function __construct($value, $checkForObjectIdentity = true, $checkForNonObjectIdentity = false) { parent::__construct(); if (!is_bool($checkForObjectIdentity)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'boolean'); } if (!is_bool($checkForNonObjectIdentity)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(3, 'boolean'); } $this->checkForObjectIdentity = $checkForObjectIdentity; $this->checkForNonObjectIdentity = $checkForNonObjectIdentity; $this->value = $value; } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { if ($other instanceof SplObjectStorage) { return $other->contains($this->value); } if (is_object($this->value)) { foreach ($other as $element) { if ($this->checkForObjectIdentity && $element === $this->value) { return true; } elseif (!$this->checkForObjectIdentity && $element == $this->value) { return true; } } } else { foreach ($other as $element) { if ($this->checkForNonObjectIdentity && $element === $this->value) { return true; } elseif (!$this->checkForNonObjectIdentity && $element == $this->value) { return true; } } } return false; } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { if (is_string($this->value) && strpos($this->value, "\n") !== false) { return 'contains "' . $this->value . '"'; } else { return 'contains ' . $this->exporter->export($this->value); } } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return sprintf( '%s %s', is_array($other) ? 'an array' : 'a traversable', $this->toString() ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Constraint that asserts that the Traversable it is applied to contains * only values of a given type. * * @since Class available since Release 3.1.4 */ class PHPUnit_Framework_Constraint_TraversableContainsOnly extends PHPUnit_Framework_Constraint { /** * @var PHPUnit_Framework_Constraint */ protected $constraint; /** * @var string */ protected $type; /** * @param string $type * @param bool $isNativeType */ public function __construct($type, $isNativeType = true) { parent::__construct(); if ($isNativeType) { $this->constraint = new PHPUnit_Framework_Constraint_IsType($type); } else { $this->constraint = new PHPUnit_Framework_Constraint_IsInstanceOf( $type ); } $this->type = $type; } /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { $success = true; foreach ($other as $item) { if (!$this->constraint->evaluate($item, '', true)) { $success = false; break; } } if ($returnResult) { return $success; } if (!$success) { $this->fail($other, $description); } } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { return 'contains only values of type "' . $this->type . '"'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Logical XOR. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_Constraint_Xor extends PHPUnit_Framework_Constraint { /** * @var PHPUnit_Framework_Constraint[] */ protected $constraints = array(); /** * @param PHPUnit_Framework_Constraint[] $constraints */ public function setConstraints(array $constraints) { $this->constraints = array(); foreach ($constraints as $constraint) { if (!($constraint instanceof PHPUnit_Framework_Constraint)) { $constraint = new PHPUnit_Framework_Constraint_IsEqual( $constraint ); } $this->constraints[] = $constraint; } } /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { $success = true; $lastResult = null; $constraint = null; foreach ($this->constraints as $constraint) { $result = $constraint->evaluate($other, $description, true); if ($result === $lastResult) { $success = false; break; } $lastResult = $result; } if ($returnResult) { return $success; } if (!$success) { $this->fail($other, $description); } } /** * Returns a string representation of the constraint. * * @return string */ public function toString() { $text = ''; foreach ($this->constraints as $key => $constraint) { if ($key > 0) { $text .= ' xor '; } $text .= $constraint->toString(); } return $text; } /** * Counts the number of constraint elements. * * @return int * * @since Method available since Release 3.4.0 */ public function count() { $count = 0; foreach ($this->constraints as $constraint) { $count += count($constraint); } return $count; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Exporter\Exporter; /** * Abstract base class for constraints which can be applied to any value. * * @since Interface available since Release 3.0.0 */ abstract class PHPUnit_Framework_Constraint implements Countable, PHPUnit_Framework_SelfDescribing { protected $exporter; public function __construct() { $this->exporter = new Exporter; } /** * Evaluates the constraint for parameter $other * * If $returnResult is set to false (the default), an exception is thrown * in case of a failure. null is returned otherwise. * * If $returnResult is true, the result of the evaluation is returned as * a boolean value instead: true in case of success, false in case of a * failure. * * @param mixed $other Value or object to evaluate. * @param string $description Additional information about the test * @param bool $returnResult Whether to return a result or throw an exception * * @return mixed * * @throws PHPUnit_Framework_ExpectationFailedException */ public function evaluate($other, $description = '', $returnResult = false) { $success = false; if ($this->matches($other)) { $success = true; } if ($returnResult) { return $success; } if (!$success) { $this->fail($other, $description); } } /** * Evaluates the constraint for parameter $other. Returns true if the * constraint is met, false otherwise. * * This method can be overridden to implement the evaluation algorithm. * * @param mixed $other Value or object to evaluate. * * @return bool */ protected function matches($other) { return false; } /** * Counts the number of constraint elements. * * @return int * * @since Method available since Release 3.4.0 */ public function count() { return 1; } /** * Throws an exception for the given compared value and test description * * @param mixed $other Evaluated value or object. * @param string $description Additional information about the test * @param SebastianBergmann\Comparator\ComparisonFailure $comparisonFailure * * @throws PHPUnit_Framework_ExpectationFailedException */ protected function fail($other, $description, SebastianBergmann\Comparator\ComparisonFailure $comparisonFailure = null) { $failureDescription = sprintf( 'Failed asserting that %s.', $this->failureDescription($other) ); $additionalFailureDescription = $this->additionalFailureDescription($other); if ($additionalFailureDescription) { $failureDescription .= "\n" . $additionalFailureDescription; } if (!empty($description)) { $failureDescription = $description . "\n" . $failureDescription; } throw new PHPUnit_Framework_ExpectationFailedException( $failureDescription, $comparisonFailure ); } /** * Return additional failure description where needed * * The function can be overridden to provide additional failure * information like a diff * * @param mixed $other Evaluated value or object. * * @return string */ protected function additionalFailureDescription($other) { return ''; } /** * Returns the description of the failure * * The beginning of failure messages is "Failed asserting that" in most * cases. This method should return the second part of that sentence. * * To provide additional failure information additionalFailureDescription * can be used. * * @param mixed $other Evaluated value or object. * * @return string */ protected function failureDescription($other) { return $this->exporter->export($other) . ' ' . $this->toString(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Wrapper for PHP deprecated errors. * You can disable deprecated-to-exception conversion by setting * * * PHPUnit_Framework_Error_Deprecated::$enabled = false; * * * @since Class available since Release 3.3.0 */ class PHPUnit_Framework_Error_Deprecated extends PHPUnit_Framework_Error { public static $enabled = true; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Wrapper for PHP notices. * You can disable notice-to-exception conversion by setting * * * PHPUnit_Framework_Error_Notice::$enabled = false; * * * @since Class available since Release 3.3.0 */ class PHPUnit_Framework_Error_Notice extends PHPUnit_Framework_Error { public static $enabled = true; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Wrapper for PHP warnings. * You can disable notice-to-exception conversion by setting * * * PHPUnit_Framework_Error_Warning::$enabled = false; * * * @since Class available since Release 3.3.0 */ class PHPUnit_Framework_Error_Warning extends PHPUnit_Framework_Error { public static $enabled = true; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Wrapper for PHP errors. * * @since Class available since Release 2.2.0 */ class PHPUnit_Framework_Error extends PHPUnit_Framework_Exception { /** * Constructor. * * @param string $message * @param int $code * @param string $file * @param int $line * @param Exception $previous */ public function __construct($message, $code, $file, $line, Exception $previous = null) { parent::__construct($message, $code, $previous); $this->file = $file; $this->line = $line; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Base class for all PHPUnit Framework exceptions. * * Ensures that exceptions thrown during a test run do not leave stray * references behind. * * Every Exception contains a stack trace. Each stack frame contains the 'args' * of the called function. The function arguments can contain references to * instantiated objects. The references prevent the objects from being * destructed (until test results are eventually printed), so memory cannot be * freed up. * * With enabled process isolation, test results are serialized in the child * process and unserialized in the parent process. The stack trace of Exceptions * may contain objects that cannot be serialized or unserialized (e.g., PDO * connections). Unserializing user-space objects from the child process into * the parent would break the intended encapsulation of process isolation. * * @see http://fabien.potencier.org/article/9/php-serialization-stack-traces-and-exceptions * @since Class available since Release 3.4.0 */ class PHPUnit_Framework_Exception extends RuntimeException implements PHPUnit_Exception { /** * @var array */ protected $serializableTrace; public function __construct($message = '', $code = 0, Exception $previous = null) { parent::__construct($message, $code, $previous); $this->serializableTrace = $this->getTrace(); foreach ($this->serializableTrace as $i => $call) { unset($this->serializableTrace[$i]['args']); } } /** * Returns the serializable trace (without 'args'). * * @return array */ public function getSerializableTrace() { return $this->serializableTrace; } /** * @return string */ public function __toString() { $string = PHPUnit_Framework_TestFailure::exceptionToString($this); if ($trace = PHPUnit_Util_Filter::getFilteredStacktrace($this)) { $string .= "\n" . $trace; } return $string; } public function __sleep() { return array_keys(get_object_vars($this)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Wraps Exceptions thrown by code under test. * * Re-instantiates Exceptions thrown by user-space code to retain their original * class names, properties, and stack traces (but without arguments). * * Unlike PHPUnit_Framework_Exception, the complete stack of previous Exceptions * is processed. * * @since Class available since Release 4.3.0 */ class PHPUnit_Framework_ExceptionWrapper extends PHPUnit_Framework_Exception { /** * @var string */ protected $classname; /** * @var PHPUnit_Framework_ExceptionWrapper|null */ protected $previous; /** * @param Throwable|Exception $e */ public function __construct($e) { // PDOException::getCode() is a string. // @see http://php.net/manual/en/class.pdoexception.php#95812 parent::__construct($e->getMessage(), (int) $e->getCode()); $this->classname = get_class($e); $this->file = $e->getFile(); $this->line = $e->getLine(); $this->serializableTrace = $e->getTrace(); foreach ($this->serializableTrace as $i => $call) { unset($this->serializableTrace[$i]['args']); } if ($e->getPrevious()) { $this->previous = new self($e->getPrevious()); } } /** * @return string */ public function getClassname() { return $this->classname; } /** * @return PHPUnit_Framework_ExceptionWrapper */ public function getPreviousWrapped() { return $this->previous; } /** * @return string */ public function __toString() { $string = PHPUnit_Framework_TestFailure::exceptionToString($this); if ($trace = PHPUnit_Util_Filter::getFilteredStacktrace($this)) { $string .= "\n" . $trace; } if ($this->previous) { $string .= "\nCaused by\n" . $this->previous; } return $string; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Exception for expectations which failed their check. * * The exception contains the error message and optionally a * SebastianBergmann\Comparator\ComparisonFailure which is used to * generate diff output of the failed expectations. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_ExpectationFailedException extends PHPUnit_Framework_AssertionFailedError { /** * @var SebastianBergmann\Comparator\ComparisonFailure */ protected $comparisonFailure; public function __construct($message, SebastianBergmann\Comparator\ComparisonFailure $comparisonFailure = null, Exception $previous = null) { $this->comparisonFailure = $comparisonFailure; parent::__construct($message, 0, $previous); } /** * @return SebastianBergmann\Comparator\ComparisonFailure */ public function getComparisonFailure() { return $this->comparisonFailure; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A marker interface for marking any exception/error as result of an unit * test as incomplete implementation or currently not implemented. * * @since Interface available since Release 2.0.0 */ interface PHPUnit_Framework_IncompleteTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * An incomplete test case * * @since Class available since Release 4.3.0 */ class PHPUnit_Framework_IncompleteTestCase extends PHPUnit_Framework_TestCase { /** * @var string */ protected $message = ''; /** * @var bool */ protected $backupGlobals = false; /** * @var bool */ protected $backupStaticAttributes = false; /** * @var bool */ protected $runTestInSeparateProcess = false; /** * @var bool */ protected $useErrorHandler = false; /** * @var bool */ protected $useOutputBuffering = false; /** * @param string $className * @param string $methodName * @param string $message */ public function __construct($className, $methodName, $message = '') { $this->message = $message; parent::__construct($className . '::' . $methodName); } /** * @throws PHPUnit_Framework_Exception */ protected function runTest() { $this->markTestIncomplete($this->message); } /** * @return string */ public function getMessage() { return $this->message; } /** * Returns a string representation of the test case. * * @return string */ public function toString() { return $this->getName(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Extension to PHPUnit_Framework_AssertionFailedError to mark the special * case of an incomplete test. * * @since Class available since Release 2.0.0 */ class PHPUnit_Framework_IncompleteTestError extends PHPUnit_Framework_AssertionFailedError implements PHPUnit_Framework_IncompleteTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Extension to PHPUnit_Framework_AssertionFailedError to mark the special * case of a test that is skipped because of an invalid @covers annotation. * * @since Class available since Release 4.0.0 */ class PHPUnit_Framework_InvalidCoversTargetError extends PHPUnit_Framework_AssertionFailedError implements PHPUnit_Framework_SkippedTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 4.0.0 */ class PHPUnit_Framework_InvalidCoversTargetException extends PHPUnit_Framework_CodeCoverageException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Extension to PHPUnit_Framework_AssertionFailedError to mark the special * case of a test that printed output. * * @since Class available since Release 3.6.0 */ class PHPUnit_Framework_OutputError extends PHPUnit_Framework_AssertionFailedError { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A marker interface for marking any exception/error as result of an unit * test as risky. * * @since Interface available since Release 4.0.0 */ interface PHPUnit_Framework_RiskyTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Extension to PHPUnit_Framework_AssertionFailedError to mark the special * case of a risky test. * * @since Class available since Release 4.0.0 */ class PHPUnit_Framework_RiskyTestError extends PHPUnit_Framework_AssertionFailedError implements PHPUnit_Framework_RiskyTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Interface for classes that can return a description of itself. * * @since Interface available since Release 3.0.0 */ interface PHPUnit_Framework_SelfDescribing { /** * Returns a string representation of the object. * * @return string */ public function toString(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A marker interface for marking a unit test as being skipped. * * @since Interface available since Release 3.0.0 */ interface PHPUnit_Framework_SkippedTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A skipped test case * * @since Class available since Release 4.3.0 */ class PHPUnit_Framework_SkippedTestCase extends PHPUnit_Framework_TestCase { /** * @var string */ protected $message = ''; /** * @var bool */ protected $backupGlobals = false; /** * @var bool */ protected $backupStaticAttributes = false; /** * @var bool */ protected $runTestInSeparateProcess = false; /** * @var bool */ protected $useErrorHandler = false; /** * @var bool */ protected $useOutputBuffering = false; /** * @param string $message */ public function __construct($className, $methodName, $message = '') { $this->message = $message; parent::__construct($className . '::' . $methodName); } /** * @throws PHPUnit_Framework_Exception */ protected function runTest() { $this->markTestSkipped($this->message); } /** * @return string */ public function getMessage() { return $this->message; } /** * Returns a string representation of the test case. * * @return string */ public function toString() { return $this->getName(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Extension to PHPUnit_Framework_AssertionFailedError to mark the special * case of a skipped test. * * @since Class available since Release 3.0.0 */ class PHPUnit_Framework_SkippedTestError extends PHPUnit_Framework_AssertionFailedError implements PHPUnit_Framework_SkippedTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Extension to PHPUnit_Framework_AssertionFailedError to mark the special * case of a skipped test suite. * * @since Class available since Release 3.1.0 */ class PHPUnit_Framework_SkippedTestSuiteError extends PHPUnit_Framework_AssertionFailedError implements PHPUnit_Framework_SkippedTest { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Creates a synthetic failed assertion. * * @since Class available since Release 3.5.0 */ class PHPUnit_Framework_SyntheticError extends PHPUnit_Framework_AssertionFailedError { /** * The synthetic file. * * @var string */ protected $syntheticFile = ''; /** * The synthetic line number. * * @var int */ protected $syntheticLine = 0; /** * The synthetic trace. * * @var array */ protected $syntheticTrace = array(); /** * Constructor. * * @param string $message * @param int $code * @param string $file * @param int $line * @param array $trace */ public function __construct($message, $code, $file, $line, $trace) { parent::__construct($message, $code); $this->syntheticFile = $file; $this->syntheticLine = $line; $this->syntheticTrace = $trace; } /** * @return string */ public function getSyntheticFile() { return $this->syntheticFile; } /** * @return int */ public function getSyntheticLine() { return $this->syntheticLine; } /** * @return array */ public function getSyntheticTrace() { return $this->syntheticTrace; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A Test can be run and collect its results. * * @since Interface available since Release 2.0.0 */ interface PHPUnit_Framework_Test extends Countable { /** * Runs a test and collects its result in a TestResult instance. * * @param PHPUnit_Framework_TestResult $result * * @return PHPUnit_Framework_TestResult */ public function run(PHPUnit_Framework_TestResult $result = null); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\GlobalState\Snapshot; use SebastianBergmann\GlobalState\Restorer; use SebastianBergmann\GlobalState\Blacklist; use SebastianBergmann\Diff\Differ; use SebastianBergmann\Exporter\Exporter; use Prophecy\Exception\Prediction\PredictionException; use Prophecy\Prophet; /** * A TestCase defines the fixture to run multiple tests. * * To define a TestCase * * 1) Implement a subclass of PHPUnit_Framework_TestCase. * 2) Define instance variables that store the state of the fixture. * 3) Initialize the fixture state by overriding setUp(). * 4) Clean-up after a test by overriding tearDown(). * * Each test runs in its own fixture so there can be no side effects * among test runs. * * Here is an example: * * * value1 = 2; * $this->value2 = 3; * } * } * ?> * * * For each test implement a method which interacts with the fixture. * Verify the expected results with assertions specified by calling * assert with a boolean. * * * assertTrue($this->value1 + $this->value2 == 5); * } * ?> * * * @since Class available since Release 2.0.0 */ abstract class PHPUnit_Framework_TestCase extends PHPUnit_Framework_Assert implements PHPUnit_Framework_Test, PHPUnit_Framework_SelfDescribing { /** * Enable or disable the backup and restoration of the $GLOBALS array. * Overwrite this attribute in a child class of TestCase. * Setting this attribute in setUp() has no effect! * * @var bool */ protected $backupGlobals = null; /** * @var array */ protected $backupGlobalsBlacklist = array(); /** * Enable or disable the backup and restoration of static attributes. * Overwrite this attribute in a child class of TestCase. * Setting this attribute in setUp() has no effect! * * @var bool */ protected $backupStaticAttributes = null; /** * @var array */ protected $backupStaticAttributesBlacklist = array(); /** * Whether or not this test is to be run in a separate PHP process. * * @var bool */ protected $runTestInSeparateProcess = null; /** * Whether or not this test should preserve the global state when * running in a separate PHP process. * * @var bool */ protected $preserveGlobalState = true; /** * Whether or not this test is running in a separate PHP process. * * @var bool */ private $inIsolation = false; /** * @var array */ private $data = array(); /** * @var string */ private $dataName = ''; /** * @var bool */ private $useErrorHandler = null; /** * The name of the expected Exception. * * @var mixed */ private $expectedException = null; /** * The message of the expected Exception. * * @var string */ private $expectedExceptionMessage = ''; /** * The regex pattern to validate the expected Exception message. * * @var string */ private $expectedExceptionMessageRegExp = ''; /** * The code of the expected Exception. * * @var int */ private $expectedExceptionCode; /** * The name of the test case. * * @var string */ private $name = null; /** * @var array */ private $dependencies = array(); /** * @var array */ private $dependencyInput = array(); /** * @var array */ private $iniSettings = array(); /** * @var array */ private $locale = array(); /** * @var array */ private $mockObjects = array(); /** * @var array */ private $mockObjectGenerator = null; /** * @var int */ private $status; /** * @var string */ private $statusMessage = ''; /** * @var int */ private $numAssertions = 0; /** * @var PHPUnit_Framework_TestResult */ private $result; /** * @var mixed */ private $testResult; /** * @var string */ private $output = ''; /** * @var string */ private $outputExpectedRegex = null; /** * @var string */ private $outputExpectedString = null; /** * @var mixed */ private $outputCallback = false; /** * @var bool */ private $outputBufferingActive = false; /** * @var int */ private $outputBufferingLevel; /** * @var SebastianBergmann\GlobalState\Snapshot */ private $snapshot; /** * @var Prophecy\Prophet */ private $prophet; /** * @var bool */ private $disallowChangesToGlobalState = false; /** * Constructs a test case with the given name. * * @param string $name * @param array $data * @param string $dataName */ public function __construct($name = null, array $data = array(), $dataName = '') { if ($name !== null) { $this->setName($name); } $this->data = $data; $this->dataName = $dataName; } /** * Returns a string representation of the test case. * * @return string */ public function toString() { $class = new ReflectionClass($this); $buffer = sprintf( '%s::%s', $class->name, $this->getName(false) ); return $buffer . $this->getDataSetAsString(); } /** * Counts the number of test cases executed by run(TestResult result). * * @return int */ public function count() { return 1; } /** * Returns the annotations for this test. * * @return array * * @since Method available since Release 3.4.0 */ public function getAnnotations() { return PHPUnit_Util_Test::parseTestMethodAnnotations( get_class($this), $this->name ); } /** * Gets the name of a TestCase. * * @param bool $withDataSet * * @return string */ public function getName($withDataSet = true) { if ($withDataSet) { return $this->name . $this->getDataSetAsString(false); } else { return $this->name; } } /** * Returns the size of the test. * * @return int * * @since Method available since Release 3.6.0 */ public function getSize() { return PHPUnit_Util_Test::getSize( get_class($this), $this->getName(false) ); } /** * @return string * * @since Method available since Release 3.6.0 */ public function getActualOutput() { if (!$this->outputBufferingActive) { return $this->output; } else { return ob_get_contents(); } } /** * @return bool * * @since Method available since Release 3.6.0 */ public function hasOutput() { if (strlen($this->output) === 0) { return false; } if ($this->hasExpectationOnOutput()) { return false; } return true; } /** * @param string $expectedRegex * * @since Method available since Release 3.6.0 * * @throws PHPUnit_Framework_Exception */ public function expectOutputRegex($expectedRegex) { if ($this->outputExpectedString !== null) { throw new PHPUnit_Framework_Exception; } if (is_string($expectedRegex) || is_null($expectedRegex)) { $this->outputExpectedRegex = $expectedRegex; } } /** * @param string $expectedString * * @since Method available since Release 3.6.0 */ public function expectOutputString($expectedString) { if ($this->outputExpectedRegex !== null) { throw new PHPUnit_Framework_Exception; } if (is_string($expectedString) || is_null($expectedString)) { $this->outputExpectedString = $expectedString; } } /** * @return bool * * @since Method available since Release 3.6.5 * @deprecated */ public function hasPerformedExpectationsOnOutput() { return $this->hasExpectationOnOutput(); } /** * @return bool * * @since Method available since Release 4.3.3 */ public function hasExpectationOnOutput() { return is_string($this->outputExpectedString) || is_string($this->outputExpectedRegex); } /** * @return string * * @since Method available since Release 3.2.0 */ public function getExpectedException() { return $this->expectedException; } /** * @param mixed $exceptionName * @param string $exceptionMessage * @param int $exceptionCode * * @since Method available since Release 3.2.0 */ public function setExpectedException($exceptionName, $exceptionMessage = '', $exceptionCode = null) { $this->expectedException = $exceptionName; $this->expectedExceptionMessage = $exceptionMessage; $this->expectedExceptionCode = $exceptionCode; } /** * @param mixed $exceptionName * @param string $exceptionMessageRegExp * @param int $exceptionCode * * @since Method available since Release 4.3.0 */ public function setExpectedExceptionRegExp($exceptionName, $exceptionMessageRegExp = '', $exceptionCode = null) { $this->expectedException = $exceptionName; $this->expectedExceptionMessageRegExp = $exceptionMessageRegExp; $this->expectedExceptionCode = $exceptionCode; } /** * @since Method available since Release 3.4.0 */ protected function setExpectedExceptionFromAnnotation() { try { $expectedException = PHPUnit_Util_Test::getExpectedException( get_class($this), $this->name ); if ($expectedException !== false) { $this->setExpectedException( $expectedException['class'], $expectedException['message'], $expectedException['code'] ); if (!empty($expectedException['message_regex'])) { $this->setExpectedExceptionRegExp( $expectedException['class'], $expectedException['message_regex'], $expectedException['code'] ); } } } catch (ReflectionException $e) { } } /** * @param bool $useErrorHandler * * @since Method available since Release 3.4.0 */ public function setUseErrorHandler($useErrorHandler) { $this->useErrorHandler = $useErrorHandler; } /** * @since Method available since Release 3.4.0 */ protected function setUseErrorHandlerFromAnnotation() { try { $useErrorHandler = PHPUnit_Util_Test::getErrorHandlerSettings( get_class($this), $this->name ); if ($useErrorHandler !== null) { $this->setUseErrorHandler($useErrorHandler); } } catch (ReflectionException $e) { } } /** * @since Method available since Release 3.6.0 */ protected function checkRequirements() { if (!$this->name || !method_exists($this, $this->name)) { return; } $missingRequirements = PHPUnit_Util_Test::getMissingRequirements( get_class($this), $this->name ); if (!empty($missingRequirements)) { $this->markTestSkipped(implode(PHP_EOL, $missingRequirements)); } } /** * Returns the status of this test. * * @return int * * @since Method available since Release 3.1.0 */ public function getStatus() { return $this->status; } /** * Returns the status message of this test. * * @return string * * @since Method available since Release 3.3.0 */ public function getStatusMessage() { return $this->statusMessage; } /** * Returns whether or not this test has failed. * * @return bool * * @since Method available since Release 3.0.0 */ public function hasFailed() { $status = $this->getStatus(); return $status == PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE || $status == PHPUnit_Runner_BaseTestRunner::STATUS_ERROR; } /** * Runs the test case and collects the results in a TestResult object. * If no TestResult object is passed a new one will be created. * * @param PHPUnit_Framework_TestResult $result * * @return PHPUnit_Framework_TestResult * * @throws PHPUnit_Framework_Exception */ public function run(PHPUnit_Framework_TestResult $result = null) { if ($result === null) { $result = $this->createResult(); } if (!$this instanceof PHPUnit_Framework_Warning) { $this->setTestResultObject($result); $this->setUseErrorHandlerFromAnnotation(); } if ($this->useErrorHandler !== null) { $oldErrorHandlerSetting = $result->getConvertErrorsToExceptions(); $result->convertErrorsToExceptions($this->useErrorHandler); } if (!$this instanceof PHPUnit_Framework_Warning && !$this->handleDependencies()) { return; } if ($this->runTestInSeparateProcess === true && $this->inIsolation !== true && !$this instanceof PHPUnit_Extensions_SeleniumTestCase && !$this instanceof PHPUnit_Extensions_PhptTestCase) { $class = new ReflectionClass($this); $template = new Text_Template( __DIR__ . '/../Util/PHP/Template/TestCaseMethod.tpl' ); if ($this->preserveGlobalState) { $constants = PHPUnit_Util_GlobalState::getConstantsAsString(); $globals = PHPUnit_Util_GlobalState::getGlobalsAsString(); $includedFiles = PHPUnit_Util_GlobalState::getIncludedFilesAsString(); $iniSettings = PHPUnit_Util_GlobalState::getIniSettingsAsString(); } else { $constants = ''; if (!empty($GLOBALS['__PHPUNIT_BOOTSTRAP'])) { $globals = '$GLOBALS[\'__PHPUNIT_BOOTSTRAP\'] = ' . var_export($GLOBALS['__PHPUNIT_BOOTSTRAP'], true) . ";\n"; } else { $globals = ''; } $includedFiles = ''; $iniSettings = ''; } $coverage = $result->getCollectCodeCoverageInformation() ? 'true' : 'false'; $isStrictAboutTestsThatDoNotTestAnything = $result->isStrictAboutTestsThatDoNotTestAnything() ? 'true' : 'false'; $isStrictAboutOutputDuringTests = $result->isStrictAboutOutputDuringTests() ? 'true' : 'false'; $isStrictAboutTestSize = $result->isStrictAboutTestSize() ? 'true' : 'false'; $isStrictAboutTodoAnnotatedTests = $result->isStrictAboutTodoAnnotatedTests() ? 'true' : 'false'; if (defined('PHPUNIT_COMPOSER_INSTALL')) { $composerAutoload = var_export(PHPUNIT_COMPOSER_INSTALL, true); } else { $composerAutoload = '\'\''; } if (defined('__PHPUNIT_PHAR__')) { $phar = var_export(__PHPUNIT_PHAR__, true); } else { $phar = '\'\''; } if ($result->getCodeCoverage()) { $codeCoverageFilter = $result->getCodeCoverage()->filter(); } else { $codeCoverageFilter = null; } $data = var_export(serialize($this->data), true); $dataName = var_export($this->dataName, true); $dependencyInput = var_export(serialize($this->dependencyInput), true); $includePath = var_export(get_include_path(), true); $codeCoverageFilter = var_export(serialize($codeCoverageFilter), true); // must do these fixes because TestCaseMethod.tpl has unserialize('{data}') in it, and we can't break BC // the lines above used to use addcslashes() rather than var_export(), which breaks null byte escape sequences $data = "'." . $data . ".'"; $dataName = "'.(" . $dataName . ").'"; $dependencyInput = "'." . $dependencyInput . ".'"; $includePath = "'." . $includePath . ".'"; $codeCoverageFilter = "'." . $codeCoverageFilter . ".'"; $configurationFilePath = (isset($GLOBALS['__PHPUNIT_CONFIGURATION_FILE']) ? $GLOBALS['__PHPUNIT_CONFIGURATION_FILE'] : ''); $template->setVar( array( 'composerAutoload' => $composerAutoload, 'phar' => $phar, 'filename' => $class->getFileName(), 'className' => $class->getName(), 'methodName' => $this->name, 'collectCodeCoverageInformation' => $coverage, 'data' => $data, 'dataName' => $dataName, 'dependencyInput' => $dependencyInput, 'constants' => $constants, 'globals' => $globals, 'include_path' => $includePath, 'included_files' => $includedFiles, 'iniSettings' => $iniSettings, 'isStrictAboutTestsThatDoNotTestAnything' => $isStrictAboutTestsThatDoNotTestAnything, 'isStrictAboutOutputDuringTests' => $isStrictAboutOutputDuringTests, 'isStrictAboutTestSize' => $isStrictAboutTestSize, 'isStrictAboutTodoAnnotatedTests' => $isStrictAboutTodoAnnotatedTests, 'codeCoverageFilter' => $codeCoverageFilter, 'configurationFilePath' => $configurationFilePath ) ); $this->prepareTemplate($template); $php = PHPUnit_Util_PHP::factory(); $php->runTestJob($template->render(), $this, $result); } else { $result->run($this); } if ($this->useErrorHandler !== null) { $result->convertErrorsToExceptions($oldErrorHandlerSetting); } $this->result = null; return $result; } /** * Runs the bare test sequence. */ public function runBare() { $this->numAssertions = 0; $this->snapshotGlobalState(); $this->startOutputBuffering(); clearstatcache(); $currentWorkingDirectory = getcwd(); $hookMethods = PHPUnit_Util_Test::getHookMethods(get_class($this)); try { $hasMetRequirements = false; $this->checkRequirements(); $hasMetRequirements = true; if ($this->inIsolation) { foreach ($hookMethods['beforeClass'] as $method) { $this->$method(); } } $this->setExpectedExceptionFromAnnotation(); foreach ($hookMethods['before'] as $method) { $this->$method(); } $this->assertPreConditions(); $this->testResult = $this->runTest(); $this->verifyMockObjects(); $this->assertPostConditions(); $this->status = PHPUnit_Runner_BaseTestRunner::STATUS_PASSED; } catch (PHPUnit_Framework_IncompleteTest $e) { $this->status = PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE; $this->statusMessage = $e->getMessage(); } catch (PHPUnit_Framework_SkippedTest $e) { $this->status = PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED; $this->statusMessage = $e->getMessage(); } catch (PHPUnit_Framework_AssertionFailedError $e) { $this->status = PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE; $this->statusMessage = $e->getMessage(); } catch (PredictionException $e) { $this->status = PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE; $this->statusMessage = $e->getMessage(); } catch (Throwable $_e) { $e = $_e; } catch (Exception $_e) { $e = $_e; } if (isset($_e)) { $this->status = PHPUnit_Runner_BaseTestRunner::STATUS_ERROR; $this->statusMessage = $_e->getMessage(); } // Clean up the mock objects. $this->mockObjects = array(); $this->prophet = null; // Tear down the fixture. An exception raised in tearDown() will be // caught and passed on when no exception was raised before. try { if ($hasMetRequirements) { foreach ($hookMethods['after'] as $method) { $this->$method(); } if ($this->inIsolation) { foreach ($hookMethods['afterClass'] as $method) { $this->$method(); } } } } catch (Throwable $_e) { if (!isset($e)) { $e = $_e; } } catch (Exception $_e) { if (!isset($e)) { $e = $_e; } } try { $this->stopOutputBuffering(); } catch (PHPUnit_Framework_RiskyTestError $_e) { if (!isset($e)) { $e = $_e; } } clearstatcache(); if ($currentWorkingDirectory != getcwd()) { chdir($currentWorkingDirectory); } $this->restoreGlobalState(); // Clean up INI settings. foreach ($this->iniSettings as $varName => $oldValue) { ini_set($varName, $oldValue); } $this->iniSettings = array(); // Clean up locale settings. foreach ($this->locale as $category => $locale) { setlocale($category, $locale); } // Perform assertion on output. if (!isset($e)) { try { if ($this->outputExpectedRegex !== null) { $this->assertRegExp($this->outputExpectedRegex, $this->output); } elseif ($this->outputExpectedString !== null) { $this->assertEquals($this->outputExpectedString, $this->output); } } catch (Throwable $_e) { $e = $_e; } catch (Exception $_e) { $e = $_e; } } // Workaround for missing "finally". if (isset($e)) { if ($e instanceof PredictionException) { $e = new PHPUnit_Framework_AssertionFailedError($e->getMessage()); } if (!$e instanceof Exception) { // Rethrow Error directly on PHP 7 as onNotSuccessfulTest does not support it throw $e; } $this->onNotSuccessfulTest($e); } } /** * Override to run the test and assert its state. * * @return mixed * * @throws Exception|PHPUnit_Framework_Exception * @throws PHPUnit_Framework_Exception */ protected function runTest() { if ($this->name === null) { throw new PHPUnit_Framework_Exception( 'PHPUnit_Framework_TestCase::$name must not be null.' ); } try { $class = new ReflectionClass($this); $method = $class->getMethod($this->name); } catch (ReflectionException $e) { $this->fail($e->getMessage()); } try { $testResult = $method->invokeArgs( $this, array_merge($this->data, $this->dependencyInput) ); } catch (Throwable $_e) { $e = $_e; } catch (Exception $_e) { $e = $_e; } if (isset($e)) { $checkException = false; if (is_string($this->expectedException)) { $checkException = true; if ($e instanceof PHPUnit_Framework_Exception) { $checkException = false; } $reflector = new ReflectionClass($this->expectedException); if ($this->expectedException === 'PHPUnit_Framework_Exception' || $this->expectedException === '\PHPUnit_Framework_Exception' || $reflector->isSubclassOf('PHPUnit_Framework_Exception')) { $checkException = true; } } if ($checkException) { $this->assertThat( $e, new PHPUnit_Framework_Constraint_Exception( $this->expectedException ) ); if (is_string($this->expectedExceptionMessage) && !empty($this->expectedExceptionMessage)) { $this->assertThat( $e, new PHPUnit_Framework_Constraint_ExceptionMessage( $this->expectedExceptionMessage ) ); } if (is_string($this->expectedExceptionMessageRegExp) && !empty($this->expectedExceptionMessageRegExp)) { $this->assertThat( $e, new PHPUnit_Framework_Constraint_ExceptionMessageRegExp( $this->expectedExceptionMessageRegExp ) ); } if ($this->expectedExceptionCode !== null) { $this->assertThat( $e, new PHPUnit_Framework_Constraint_ExceptionCode( $this->expectedExceptionCode ) ); } return; } else { throw $e; } } if ($this->expectedException !== null) { $this->assertThat( null, new PHPUnit_Framework_Constraint_Exception( $this->expectedException ) ); } return $testResult; } /** * Verifies the mock object expectations. * * @since Method available since Release 3.5.0 */ protected function verifyMockObjects() { foreach ($this->mockObjects as $mockObject) { if ($mockObject->__phpunit_hasMatchers()) { $this->numAssertions++; } $mockObject->__phpunit_verify(); } if ($this->prophet !== null) { try { $this->prophet->checkPredictions(); } catch (Throwable $e) { /* Intentionally left empty */ } catch (Exception $e) { /* Intentionally left empty */ } foreach ($this->prophet->getProphecies() as $objectProphecy) { foreach ($objectProphecy->getMethodProphecies() as $methodProphecies) { foreach ($methodProphecies as $methodProphecy) { $this->numAssertions += count($methodProphecy->getCheckedPredictions()); } } } if (isset($e)) { throw $e; } } } /** * Sets the name of a TestCase. * * @param string */ public function setName($name) { $this->name = $name; } /** * Sets the dependencies of a TestCase. * * @param array $dependencies * * @since Method available since Release 3.4.0 */ public function setDependencies(array $dependencies) { $this->dependencies = $dependencies; } /** * Returns true if the tests has dependencies * * @return bool * * @since Method available since Release 4.0.0 */ public function hasDependencies() { return count($this->dependencies) > 0; } /** * Sets * * @param array $dependencyInput * * @since Method available since Release 3.4.0 */ public function setDependencyInput(array $dependencyInput) { $this->dependencyInput = $dependencyInput; } /** * @param bool $disallowChangesToGlobalState * * @since Method available since Release 4.6.0 */ public function setDisallowChangesToGlobalState($disallowChangesToGlobalState) { $this->disallowChangesToGlobalState = $disallowChangesToGlobalState; } /** * Calling this method in setUp() has no effect! * * @param bool $backupGlobals * * @since Method available since Release 3.3.0 */ public function setBackupGlobals($backupGlobals) { if (is_null($this->backupGlobals) && is_bool($backupGlobals)) { $this->backupGlobals = $backupGlobals; } } /** * Calling this method in setUp() has no effect! * * @param bool $backupStaticAttributes * * @since Method available since Release 3.4.0 */ public function setBackupStaticAttributes($backupStaticAttributes) { if (is_null($this->backupStaticAttributes) && is_bool($backupStaticAttributes)) { $this->backupStaticAttributes = $backupStaticAttributes; } } /** * @param bool $runTestInSeparateProcess * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.4.0 */ public function setRunTestInSeparateProcess($runTestInSeparateProcess) { if (is_bool($runTestInSeparateProcess)) { if ($this->runTestInSeparateProcess === null) { $this->runTestInSeparateProcess = $runTestInSeparateProcess; } } else { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } } /** * @param bool $preserveGlobalState * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.4.0 */ public function setPreserveGlobalState($preserveGlobalState) { if (is_bool($preserveGlobalState)) { $this->preserveGlobalState = $preserveGlobalState; } else { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } } /** * @param bool $inIsolation * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.4.0 */ public function setInIsolation($inIsolation) { if (is_bool($inIsolation)) { $this->inIsolation = $inIsolation; } else { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } } /** * @return bool * * @since Method available since Release 4.3.0 */ public function isInIsolation() { return $this->inIsolation; } /** * @return mixed * * @since Method available since Release 3.4.0 */ public function getResult() { return $this->testResult; } /** * @param mixed $result * * @since Method available since Release 3.4.0 */ public function setResult($result) { $this->testResult = $result; } /** * @param callable $callback * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.6.0 */ public function setOutputCallback($callback) { if (!is_callable($callback)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'callback'); } $this->outputCallback = $callback; } /** * @return PHPUnit_Framework_TestResult * * @since Method available since Release 3.5.7 */ public function getTestResultObject() { return $this->result; } /** * @param PHPUnit_Framework_TestResult $result * * @since Method available since Release 3.6.0 */ public function setTestResultObject(PHPUnit_Framework_TestResult $result) { $this->result = $result; } /** * This method is a wrapper for the ini_set() function that automatically * resets the modified php.ini setting to its original value after the * test is run. * * @param string $varName * @param string $newValue * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.0.0 */ protected function iniSet($varName, $newValue) { if (!is_string($varName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } $currentValue = ini_set($varName, $newValue); if ($currentValue !== false) { $this->iniSettings[$varName] = $currentValue; } else { throw new PHPUnit_Framework_Exception( sprintf( 'INI setting "%s" could not be set to "%s".', $varName, $newValue ) ); } } /** * This method is a wrapper for the setlocale() function that automatically * resets the locale to its original value after the test is run. * * @param int $category * @param string $locale * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.1.0 */ protected function setLocale() { $args = func_get_args(); if (count($args) < 2) { throw new PHPUnit_Framework_Exception; } $category = $args[0]; $locale = $args[1]; $categories = array( LC_ALL, LC_COLLATE, LC_CTYPE, LC_MONETARY, LC_NUMERIC, LC_TIME ); if (defined('LC_MESSAGES')) { $categories[] = LC_MESSAGES; } if (!in_array($category, $categories)) { throw new PHPUnit_Framework_Exception; } if (!is_array($locale) && !is_string($locale)) { throw new PHPUnit_Framework_Exception; } $this->locale[$category] = setlocale($category, null); $result = call_user_func_array('setlocale', $args); if ($result === false) { throw new PHPUnit_Framework_Exception( 'The locale functionality is not implemented on your platform, ' . 'the specified locale does not exist or the category name is ' . 'invalid.' ); } } /** * Returns a mock object for the specified class. * * @param string $originalClassName Name of the class to mock. * @param array|null $methods When provided, only methods whose names are in the array * are replaced with a configurable test double. The behavior * of the other methods is not changed. * Providing null means that no methods will be replaced. * @param array $arguments Parameters to pass to the original class' constructor. * @param string $mockClassName Class name for the generated test double class. * @param bool $callOriginalConstructor Can be used to disable the call to the original class' constructor. * @param bool $callOriginalClone Can be used to disable the call to the original class' clone constructor. * @param bool $callAutoload Can be used to disable __autoload() during the generation of the test double class. * @param bool $cloneArguments * @param bool $callOriginalMethods * @param object $proxyTarget * * @return PHPUnit_Framework_MockObject_MockObject * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.0.0 */ public function getMock($originalClassName, $methods = array(), array $arguments = array(), $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = false, $callOriginalMethods = false, $proxyTarget = null) { $mockObject = $this->getMockObjectGenerator()->getMock( $originalClassName, $methods, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods, $proxyTarget ); $this->mockObjects[] = $mockObject; return $mockObject; } /** * Returns a builder object to create mock objects using a fluent interface. * * @param string $className * * @return PHPUnit_Framework_MockObject_MockBuilder * * @since Method available since Release 3.5.0 */ public function getMockBuilder($className) { return new PHPUnit_Framework_MockObject_MockBuilder($this, $className); } /** * Mocks the specified class and returns the name of the mocked class. * * @param string $originalClassName * @param array $methods * @param array $arguments * @param string $mockClassName * @param bool $callOriginalConstructor * @param bool $callOriginalClone * @param bool $callAutoload * @param bool $cloneArguments * * @return string * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.5.0 */ protected function getMockClass($originalClassName, $methods = array(), array $arguments = array(), $mockClassName = '', $callOriginalConstructor = false, $callOriginalClone = true, $callAutoload = true, $cloneArguments = false) { $mock = $this->getMock( $originalClassName, $methods, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $cloneArguments ); return get_class($mock); } /** * Returns a mock object for the specified abstract class with all abstract * methods of the class mocked. Concrete methods are not mocked by default. * To mock concrete methods, use the 7th parameter ($mockedMethods). * * @param string $originalClassName * @param array $arguments * @param string $mockClassName * @param bool $callOriginalConstructor * @param bool $callOriginalClone * @param bool $callAutoload * @param array $mockedMethods * @param bool $cloneArguments * * @return PHPUnit_Framework_MockObject_MockObject * * @since Method available since Release 3.4.0 * * @throws PHPUnit_Framework_Exception */ public function getMockForAbstractClass($originalClassName, array $arguments = array(), $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = array(), $cloneArguments = false) { $mockObject = $this->getMockObjectGenerator()->getMockForAbstractClass( $originalClassName, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments ); $this->mockObjects[] = $mockObject; return $mockObject; } /** * Returns a mock object based on the given WSDL file. * * @param string $wsdlFile * @param string $originalClassName * @param string $mockClassName * @param array $methods * @param bool $callOriginalConstructor * @param array $options An array of options passed to SOAPClient::_construct * * @return PHPUnit_Framework_MockObject_MockObject * * @since Method available since Release 3.4.0 */ protected function getMockFromWsdl($wsdlFile, $originalClassName = '', $mockClassName = '', array $methods = array(), $callOriginalConstructor = true, array $options = array()) { if ($originalClassName === '') { $originalClassName = str_replace('.wsdl', '', basename($wsdlFile)); } if (!class_exists($originalClassName)) { eval( $this->getMockObjectGenerator()->generateClassFromWsdl( $wsdlFile, $originalClassName, $methods, $options ) ); } return $this->getMock( $originalClassName, $methods, array('', $options), $mockClassName, $callOriginalConstructor, false, false ); } /** * Returns a mock object for the specified trait with all abstract methods * of the trait mocked. Concrete methods to mock can be specified with the * `$mockedMethods` parameter. * * @param string $traitName * @param array $arguments * @param string $mockClassName * @param bool $callOriginalConstructor * @param bool $callOriginalClone * @param bool $callAutoload * @param array $mockedMethods * @param bool $cloneArguments * * @return PHPUnit_Framework_MockObject_MockObject * * @since Method available since Release 4.0.0 * * @throws PHPUnit_Framework_Exception */ public function getMockForTrait($traitName, array $arguments = array(), $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = array(), $cloneArguments = false) { $mockObject = $this->getMockObjectGenerator()->getMockForTrait( $traitName, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments ); $this->mockObjects[] = $mockObject; return $mockObject; } /** * Returns an object for the specified trait. * * @param string $traitName * @param array $arguments * @param string $traitClassName * @param bool $callOriginalConstructor * @param bool $callOriginalClone * @param bool $callAutoload * @param bool $cloneArguments * * @return object * * @since Method available since Release 3.6.0 * * @throws PHPUnit_Framework_Exception */ protected function getObjectForTrait($traitName, array $arguments = array(), $traitClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = false) { return $this->getMockObjectGenerator()->getObjectForTrait( $traitName, $arguments, $traitClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $cloneArguments ); } /** * @param string|null $classOrInterface * * @return \Prophecy\Prophecy\ObjectProphecy * * @throws \LogicException * * @since Method available since Release 4.5.0 */ protected function prophesize($classOrInterface = null) { return $this->getProphet()->prophesize($classOrInterface); } /** * Adds a value to the assertion counter. * * @param int $count * * @since Method available since Release 3.3.3 */ public function addToAssertionCount($count) { $this->numAssertions += $count; } /** * Returns the number of assertions performed by this test. * * @return int * * @since Method available since Release 3.3.0 */ public function getNumAssertions() { return $this->numAssertions; } /** * Returns a matcher that matches when the method is executed * zero or more times. * * @return PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount * * @since Method available since Release 3.0.0 */ public static function any() { return new PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount; } /** * Returns a matcher that matches when the method is never executed. * * @return PHPUnit_Framework_MockObject_Matcher_InvokedCount * * @since Method available since Release 3.0.0 */ public static function never() { return new PHPUnit_Framework_MockObject_Matcher_InvokedCount(0); } /** * Returns a matcher that matches when the method is executed * at least N times. * * @param int $requiredInvocations * * @return PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastCount * * @since Method available since Release 4.2.0 */ public static function atLeast($requiredInvocations) { return new PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastCount( $requiredInvocations ); } /** * Returns a matcher that matches when the method is executed at least once. * * @return PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce * * @since Method available since Release 3.0.0 */ public static function atLeastOnce() { return new PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce; } /** * Returns a matcher that matches when the method is executed exactly once. * * @return PHPUnit_Framework_MockObject_Matcher_InvokedCount * * @since Method available since Release 3.0.0 */ public static function once() { return new PHPUnit_Framework_MockObject_Matcher_InvokedCount(1); } /** * Returns a matcher that matches when the method is executed * exactly $count times. * * @param int $count * * @return PHPUnit_Framework_MockObject_Matcher_InvokedCount * * @since Method available since Release 3.0.0 */ public static function exactly($count) { return new PHPUnit_Framework_MockObject_Matcher_InvokedCount($count); } /** * Returns a matcher that matches when the method is executed * at most N times. * * @param int $allowedInvocations * * @return PHPUnit_Framework_MockObject_Matcher_InvokedAtMostCount * * @since Method available since Release 4.2.0 */ public static function atMost($allowedInvocations) { return new PHPUnit_Framework_MockObject_Matcher_InvokedAtMostCount( $allowedInvocations ); } /** * Returns a matcher that matches when the method is executed * at the given index. * * @param int $index * * @return PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex * * @since Method available since Release 3.0.0 */ public static function at($index) { return new PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex($index); } /** * @param mixed $value * * @return PHPUnit_Framework_MockObject_Stub_Return * * @since Method available since Release 3.0.0 */ public static function returnValue($value) { return new PHPUnit_Framework_MockObject_Stub_Return($value); } /** * @param array $valueMap * * @return PHPUnit_Framework_MockObject_Stub_ReturnValueMap * * @since Method available since Release 3.6.0 */ public static function returnValueMap(array $valueMap) { return new PHPUnit_Framework_MockObject_Stub_ReturnValueMap($valueMap); } /** * @param int $argumentIndex * * @return PHPUnit_Framework_MockObject_Stub_ReturnArgument * * @since Method available since Release 3.3.0 */ public static function returnArgument($argumentIndex) { return new PHPUnit_Framework_MockObject_Stub_ReturnArgument( $argumentIndex ); } /** * @param mixed $callback * * @return PHPUnit_Framework_MockObject_Stub_ReturnCallback * * @since Method available since Release 3.3.0 */ public static function returnCallback($callback) { return new PHPUnit_Framework_MockObject_Stub_ReturnCallback($callback); } /** * Returns the current object. * * This method is useful when mocking a fluent interface. * * @return PHPUnit_Framework_MockObject_Stub_ReturnSelf * * @since Method available since Release 3.6.0 */ public static function returnSelf() { return new PHPUnit_Framework_MockObject_Stub_ReturnSelf(); } /** * @param Exception $exception * * @return PHPUnit_Framework_MockObject_Stub_Exception * * @since Method available since Release 3.1.0 */ public static function throwException(Exception $exception) { return new PHPUnit_Framework_MockObject_Stub_Exception($exception); } /** * @param mixed $value, ... * * @return PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls * * @since Method available since Release 3.0.0 */ public static function onConsecutiveCalls() { $args = func_get_args(); return new PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls($args); } /** * Gets the data set description of a TestCase. * * @param bool $includeData * * @return string * * @since Method available since Release 3.3.0 */ protected function getDataSetAsString($includeData = true) { $buffer = ''; if (!empty($this->data)) { if (is_int($this->dataName)) { $buffer .= sprintf(' with data set #%d', $this->dataName); } else { $buffer .= sprintf(' with data set "%s"', $this->dataName); } $exporter = new Exporter; if ($includeData) { $buffer .= sprintf(' (%s)', $exporter->shortenedRecursiveExport($this->data)); } } return $buffer; } /** * Creates a default TestResult object. * * @return PHPUnit_Framework_TestResult */ protected function createResult() { return new PHPUnit_Framework_TestResult; } /** * @since Method available since Release 3.5.4 */ protected function handleDependencies() { if (!empty($this->dependencies) && !$this->inIsolation) { $className = get_class($this); $passed = $this->result->passed(); $passedKeys = array_keys($passed); $numKeys = count($passedKeys); for ($i = 0; $i < $numKeys; $i++) { $pos = strpos($passedKeys[$i], ' with data set'); if ($pos !== false) { $passedKeys[$i] = substr($passedKeys[$i], 0, $pos); } } $passedKeys = array_flip(array_unique($passedKeys)); foreach ($this->dependencies as $dependency) { if (strpos($dependency, '::') === false) { $dependency = $className . '::' . $dependency; } if (!isset($passedKeys[$dependency])) { $this->result->addError( $this, new PHPUnit_Framework_SkippedTestError( sprintf( 'This test depends on "%s" to pass.', $dependency ) ), 0 ); return false; } if (isset($passed[$dependency])) { if ($passed[$dependency]['size'] != PHPUnit_Util_Test::UNKNOWN && $this->getSize() != PHPUnit_Util_Test::UNKNOWN && $passed[$dependency]['size'] > $this->getSize()) { $this->result->addError( $this, new PHPUnit_Framework_SkippedTestError( 'This test depends on a test that is larger than itself.' ), 0 ); return false; } $this->dependencyInput[$dependency] = $passed[$dependency]['result']; } else { $this->dependencyInput[$dependency] = null; } } } return true; } /** * This method is called before the first test of this test class is run. * * @since Method available since Release 3.4.0 */ public static function setUpBeforeClass() { } /** * Sets up the fixture, for example, open a network connection. * This method is called before a test is executed. */ protected function setUp() { } /** * Performs assertions shared by all tests of a test case. * * This method is called before the execution of a test starts * and after setUp() is called. * * @since Method available since Release 3.2.8 */ protected function assertPreConditions() { } /** * Performs assertions shared by all tests of a test case. * * This method is called before the execution of a test ends * and before tearDown() is called. * * @since Method available since Release 3.2.8 */ protected function assertPostConditions() { } /** * Tears down the fixture, for example, close a network connection. * This method is called after a test is executed. */ protected function tearDown() { } /** * This method is called after the last test of this test class is run. * * @since Method available since Release 3.4.0 */ public static function tearDownAfterClass() { } /** * This method is called when a test method did not execute successfully. * * @param Exception $e * * @since Method available since Release 3.4.0 * * @throws Exception */ protected function onNotSuccessfulTest(Exception $e) { throw $e; } /** * Performs custom preparations on the process isolation template. * * @param Text_Template $template * * @since Method available since Release 3.4.0 */ protected function prepareTemplate(Text_Template $template) { } /** * Get the mock object generator, creating it if it doesn't exist. * * @return PHPUnit_Framework_MockObject_Generator */ protected function getMockObjectGenerator() { if (null === $this->mockObjectGenerator) { $this->mockObjectGenerator = new PHPUnit_Framework_MockObject_Generator; } return $this->mockObjectGenerator; } /** * @since Method available since Release 4.2.0 */ private function startOutputBuffering() { while (!defined('PHPUNIT_TESTSUITE') && ob_get_level() > 0) { ob_end_clean(); } ob_start(); $this->outputBufferingActive = true; $this->outputBufferingLevel = ob_get_level(); } /** * @since Method available since Release 4.2.0 */ private function stopOutputBuffering() { if (ob_get_level() != $this->outputBufferingLevel) { while (ob_get_level() > 0) { ob_end_clean(); } throw new PHPUnit_Framework_RiskyTestError( 'Test code or tested code did not (only) close its own output buffers' ); } $output = ob_get_contents(); if ($this->outputCallback === false) { $this->output = $output; } else { $this->output = call_user_func_array( $this->outputCallback, array($output) ); } ob_end_clean(); $this->outputBufferingActive = false; $this->outputBufferingLevel = ob_get_level(); } private function snapshotGlobalState() { $backupGlobals = $this->backupGlobals === null || $this->backupGlobals === true; if ($this->runTestInSeparateProcess || $this->inIsolation || (!$backupGlobals && !$this->backupStaticAttributes)) { return; } $this->snapshot = $this->createGlobalStateSnapshot($backupGlobals); } private function restoreGlobalState() { if (!$this->snapshot instanceof Snapshot) { return; } $backupGlobals = $this->backupGlobals === null || $this->backupGlobals === true; if ($this->disallowChangesToGlobalState) { try { $this->compareGlobalStateSnapshots( $this->snapshot, $this->createGlobalStateSnapshot($backupGlobals) ); } catch (PHPUnit_Framework_RiskyTestError $rte) { // Intentionally left empty } } $restorer = new Restorer; if ($backupGlobals) { $restorer->restoreGlobalVariables($this->snapshot); } if ($this->backupStaticAttributes) { $restorer->restoreStaticAttributes($this->snapshot); } $this->snapshot = null; if (isset($rte)) { throw $rte; } } /** * @param bool $backupGlobals * * @return Snapshot */ private function createGlobalStateSnapshot($backupGlobals) { $blacklist = new Blacklist; foreach ($this->backupGlobalsBlacklist as $globalVariable) { $blacklist->addGlobalVariable($globalVariable); } if (!defined('PHPUNIT_TESTSUITE')) { $blacklist->addClassNamePrefix('PHPUnit'); $blacklist->addClassNamePrefix('File_Iterator'); $blacklist->addClassNamePrefix('PHP_CodeCoverage'); $blacklist->addClassNamePrefix('PHP_Invoker'); $blacklist->addClassNamePrefix('PHP_Timer'); $blacklist->addClassNamePrefix('PHP_Token'); $blacklist->addClassNamePrefix('Symfony'); $blacklist->addClassNamePrefix('Text_Template'); $blacklist->addClassNamePrefix('Doctrine\Instantiator'); foreach ($this->backupStaticAttributesBlacklist as $class => $attributes) { foreach ($attributes as $attribute) { $blacklist->addStaticAttribute($class, $attribute); } } } return new Snapshot( $blacklist, $backupGlobals, $this->backupStaticAttributes, false, false, false, false, false, false, false ); } /** * @param Snapshot $before * @param Snapshot $after * * @throws PHPUnit_Framework_RiskyTestError */ private function compareGlobalStateSnapshots(Snapshot $before, Snapshot $after) { $backupGlobals = $this->backupGlobals === null || $this->backupGlobals === true; if ($backupGlobals) { $this->compareGlobalStateSnapshotPart( $before->globalVariables(), $after->globalVariables(), "--- Global variables before the test\n+++ Global variables after the test\n" ); $this->compareGlobalStateSnapshotPart( $before->superGlobalVariables(), $after->superGlobalVariables(), "--- Super-global variables before the test\n+++ Super-global variables after the test\n" ); } if ($this->backupStaticAttributes) { $this->compareGlobalStateSnapshotPart( $before->staticAttributes(), $after->staticAttributes(), "--- Static attributes before the test\n+++ Static attributes after the test\n" ); } } /** * @param array $before * @param array $after * @param string $header * * @throws PHPUnit_Framework_RiskyTestError */ private function compareGlobalStateSnapshotPart(array $before, array $after, $header) { if ($before != $after) { $differ = new Differ($header); $exporter = new Exporter; $diff = $differ->diff( $exporter->export($before), $exporter->export($after) ); throw new PHPUnit_Framework_RiskyTestError( $diff ); } } /** * @return Prophecy\Prophet * * @since Method available since Release 4.5.0 */ private function getProphet() { if ($this->prophet === null) { $this->prophet = new Prophet; } return $this->prophet; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A TestFailure collects a failed test together with the caught exception. * * @since Class available since Release 2.0.0 */ class PHPUnit_Framework_TestFailure { /** * @var string */ private $testName; /** * @var PHPUnit_Framework_Test|null */ protected $failedTest; /** * @var Exception */ protected $thrownException; /** * Constructs a TestFailure with the given test and exception. * * @param PHPUnit_Framework_Test $failedTest * @param Exception $thrownException */ public function __construct(PHPUnit_Framework_Test $failedTest, Exception $thrownException) { if ($failedTest instanceof PHPUnit_Framework_SelfDescribing) { $this->testName = $failedTest->toString(); } else { $this->testName = get_class($failedTest); } if (!$failedTest instanceof PHPUnit_Framework_TestCase || !$failedTest->isInIsolation()) { $this->failedTest = $failedTest; } $this->thrownException = $thrownException; } /** * Returns a short description of the failure. * * @return string */ public function toString() { return sprintf( '%s: %s', $this->testName, $this->thrownException->getMessage() ); } /** * Returns a description for the thrown exception. * * @return string * * @since Method available since Release 3.4.0 */ public function getExceptionAsString() { return self::exceptionToString($this->thrownException); } /** * Returns a description for an exception. * * @param Exception $e * * @return string * * @since Method available since Release 3.2.0 */ public static function exceptionToString(Exception $e) { if ($e instanceof PHPUnit_Framework_SelfDescribing) { $buffer = $e->toString(); if ($e instanceof PHPUnit_Framework_ExpectationFailedException && $e->getComparisonFailure()) { $buffer = $buffer . $e->getComparisonFailure()->getDiff(); } if (!empty($buffer)) { $buffer = trim($buffer) . "\n"; } } elseif ($e instanceof PHPUnit_Framework_Error) { $buffer = $e->getMessage() . "\n"; } elseif ($e instanceof PHPUnit_Framework_ExceptionWrapper) { $buffer = $e->getClassname() . ': ' . $e->getMessage() . "\n"; } else { $buffer = get_class($e) . ': ' . $e->getMessage() . "\n"; } return $buffer; } /** * Returns the name of the failing test (including data set, if any). * * @return string * * @since Method available since Release 4.3.0 */ public function getTestName() { return $this->testName; } /** * Returns the failing test. * * Note: The test object is not set when the test is executed in process * isolation. * * @see PHPUnit_Framework_Exception * * @return PHPUnit_Framework_Test|null */ public function failedTest() { return $this->failedTest; } /** * Gets the thrown exception. * * @return Exception */ public function thrownException() { return $this->thrownException; } /** * Returns the exception's message. * * @return string */ public function exceptionMessage() { return $this->thrownException()->getMessage(); } /** * Returns true if the thrown exception * is of type AssertionFailedError. * * @return bool */ public function isFailure() { return ($this->thrownException() instanceof PHPUnit_Framework_AssertionFailedError); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A Listener for test progress. * * @since Interface available since Release 2.0.0 */ interface PHPUnit_Framework_TestListener { /** * An error occurred. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addError(PHPUnit_Framework_Test $test, Exception $e, $time); /** * A failure occurred. * * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_AssertionFailedError $e * @param float $time */ public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time); /** * Incomplete test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time); /** * Risky test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 4.0.0 */ public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time); /** * Skipped test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 3.0.0 */ public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time); /** * A test suite started. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function startTestSuite(PHPUnit_Framework_TestSuite $suite); /** * A test suite ended. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function endTestSuite(PHPUnit_Framework_TestSuite $suite); /** * A test started. * * @param PHPUnit_Framework_Test $test */ public function startTest(PHPUnit_Framework_Test $test); /** * A test ended. * * @param PHPUnit_Framework_Test $test * @param float $time */ public function endTest(PHPUnit_Framework_Test $test, $time); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A TestResult collects the results of executing a test case. * * @since Class available since Release 2.0.0 */ class PHPUnit_Framework_TestResult implements Countable { /** * @var array */ protected $passed = array(); /** * @var array */ protected $errors = array(); /** * @var array */ protected $failures = array(); /** * @var array */ protected $notImplemented = array(); /** * @var array */ protected $risky = array(); /** * @var array */ protected $skipped = array(); /** * @var array */ protected $listeners = array(); /** * @var int */ protected $runTests = 0; /** * @var float */ protected $time = 0; /** * @var PHPUnit_Framework_TestSuite */ protected $topTestSuite = null; /** * Code Coverage information. * * @var PHP_CodeCoverage */ protected $codeCoverage; /** * @var bool */ protected $convertErrorsToExceptions = true; /** * @var bool */ protected $stop = false; /** * @var bool */ protected $stopOnError = false; /** * @var bool */ protected $stopOnFailure = false; /** * @var bool */ protected $beStrictAboutTestsThatDoNotTestAnything = false; /** * @var bool */ protected $beStrictAboutOutputDuringTests = false; /** * @var bool */ protected $beStrictAboutTestSize = false; /** * @var bool */ protected $beStrictAboutTodoAnnotatedTests = false; /** * @var bool */ protected $stopOnRisky = false; /** * @var bool */ protected $stopOnIncomplete = false; /** * @var bool */ protected $stopOnSkipped = false; /** * @var bool */ protected $lastTestFailed = false; /** * @var int */ protected $timeoutForSmallTests = 1; /** * @var int */ protected $timeoutForMediumTests = 10; /** * @var int */ protected $timeoutForLargeTests = 60; /** * Registers a TestListener. * * @param PHPUnit_Framework_TestListener */ public function addListener(PHPUnit_Framework_TestListener $listener) { $this->listeners[] = $listener; } /** * Unregisters a TestListener. * * @param PHPUnit_Framework_TestListener $listener */ public function removeListener(PHPUnit_Framework_TestListener $listener) { foreach ($this->listeners as $key => $_listener) { if ($listener === $_listener) { unset($this->listeners[$key]); } } } /** * Flushes all flushable TestListeners. * * @since Method available since Release 3.0.0 */ public function flushListeners() { foreach ($this->listeners as $listener) { if ($listener instanceof PHPUnit_Util_Printer) { $listener->flush(); } } } /** * Adds an error to the list of errors. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { if ($e instanceof PHPUnit_Framework_RiskyTest) { $this->risky[] = new PHPUnit_Framework_TestFailure($test, $e); $notifyMethod = 'addRiskyTest'; if ($this->stopOnRisky) { $this->stop(); } } elseif ($e instanceof PHPUnit_Framework_IncompleteTest) { $this->notImplemented[] = new PHPUnit_Framework_TestFailure($test, $e); $notifyMethod = 'addIncompleteTest'; if ($this->stopOnIncomplete) { $this->stop(); } } elseif ($e instanceof PHPUnit_Framework_SkippedTest) { $this->skipped[] = new PHPUnit_Framework_TestFailure($test, $e); $notifyMethod = 'addSkippedTest'; if ($this->stopOnSkipped) { $this->stop(); } } else { $this->errors[] = new PHPUnit_Framework_TestFailure($test, $e); $notifyMethod = 'addError'; if ($this->stopOnError || $this->stopOnFailure) { $this->stop(); } } foreach ($this->listeners as $listener) { $listener->$notifyMethod($test, $e, $time); } $this->lastTestFailed = true; $this->time += $time; } /** * Adds a failure to the list of failures. * The passed in exception caused the failure. * * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_AssertionFailedError $e * @param float $time */ public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { if ($e instanceof PHPUnit_Framework_RiskyTest || $e instanceof PHPUnit_Framework_OutputError) { $this->risky[] = new PHPUnit_Framework_TestFailure($test, $e); $notifyMethod = 'addRiskyTest'; if ($this->stopOnRisky) { $this->stop(); } } elseif ($e instanceof PHPUnit_Framework_IncompleteTest) { $this->notImplemented[] = new PHPUnit_Framework_TestFailure($test, $e); $notifyMethod = 'addIncompleteTest'; if ($this->stopOnIncomplete) { $this->stop(); } } elseif ($e instanceof PHPUnit_Framework_SkippedTest) { $this->skipped[] = new PHPUnit_Framework_TestFailure($test, $e); $notifyMethod = 'addSkippedTest'; if ($this->stopOnSkipped) { $this->stop(); } } else { $this->failures[] = new PHPUnit_Framework_TestFailure($test, $e); $notifyMethod = 'addFailure'; if ($this->stopOnFailure) { $this->stop(); } } foreach ($this->listeners as $listener) { $listener->$notifyMethod($test, $e, $time); } $this->lastTestFailed = true; $this->time += $time; } /** * Informs the result that a testsuite will be started. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function startTestSuite(PHPUnit_Framework_TestSuite $suite) { if ($this->topTestSuite === null) { $this->topTestSuite = $suite; } foreach ($this->listeners as $listener) { $listener->startTestSuite($suite); } } /** * Informs the result that a testsuite was completed. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function endTestSuite(PHPUnit_Framework_TestSuite $suite) { foreach ($this->listeners as $listener) { $listener->endTestSuite($suite); } } /** * Informs the result that a test will be started. * * @param PHPUnit_Framework_Test $test */ public function startTest(PHPUnit_Framework_Test $test) { $this->lastTestFailed = false; $this->runTests += count($test); foreach ($this->listeners as $listener) { $listener->startTest($test); } } /** * Informs the result that a test was completed. * * @param PHPUnit_Framework_Test $test * @param float $time */ public function endTest(PHPUnit_Framework_Test $test, $time) { foreach ($this->listeners as $listener) { $listener->endTest($test, $time); } if (!$this->lastTestFailed && $test instanceof PHPUnit_Framework_TestCase) { $class = get_class($test); $key = $class . '::' . $test->getName(); $this->passed[$key] = array( 'result' => $test->getResult(), 'size' => PHPUnit_Util_Test::getSize( $class, $test->getName(false) ) ); $this->time += $time; } } /** * Returns true if no risky test occurred. * * @return bool * * @since Method available since Release 4.0.0 */ public function allHarmless() { return $this->riskyCount() == 0; } /** * Gets the number of risky tests. * * @return int * * @since Method available since Release 4.0.0 */ public function riskyCount() { return count($this->risky); } /** * Returns true if no incomplete test occurred. * * @return bool */ public function allCompletelyImplemented() { return $this->notImplementedCount() == 0; } /** * Gets the number of incomplete tests. * * @return int */ public function notImplementedCount() { return count($this->notImplemented); } /** * Returns an Enumeration for the risky tests. * * @return array * * @since Method available since Release 4.0.0 */ public function risky() { return $this->risky; } /** * Returns an Enumeration for the incomplete tests. * * @return array */ public function notImplemented() { return $this->notImplemented; } /** * Returns true if no test has been skipped. * * @return bool * * @since Method available since Release 3.0.0 */ public function noneSkipped() { return $this->skippedCount() == 0; } /** * Gets the number of skipped tests. * * @return int * * @since Method available since Release 3.0.0 */ public function skippedCount() { return count($this->skipped); } /** * Returns an Enumeration for the skipped tests. * * @return array * * @since Method available since Release 3.0.0 */ public function skipped() { return $this->skipped; } /** * Gets the number of detected errors. * * @return int */ public function errorCount() { return count($this->errors); } /** * Returns an Enumeration for the errors. * * @return array */ public function errors() { return $this->errors; } /** * Gets the number of detected failures. * * @return int */ public function failureCount() { return count($this->failures); } /** * Returns an Enumeration for the failures. * * @return array */ public function failures() { return $this->failures; } /** * Returns the names of the tests that have passed. * * @return array * * @since Method available since Release 3.4.0 */ public function passed() { return $this->passed; } /** * Returns the (top) test suite. * * @return PHPUnit_Framework_TestSuite * * @since Method available since Release 3.0.0 */ public function topTestSuite() { return $this->topTestSuite; } /** * Returns whether code coverage information should be collected. * * @return bool If code coverage should be collected * * @since Method available since Release 3.2.0 */ public function getCollectCodeCoverageInformation() { return $this->codeCoverage !== null; } /** * Runs a TestCase. * * @param PHPUnit_Framework_Test $test */ public function run(PHPUnit_Framework_Test $test) { PHPUnit_Framework_Assert::resetCount(); $error = false; $failure = false; $incomplete = false; $risky = false; $skipped = false; $this->startTest($test); $errorHandlerSet = false; if ($this->convertErrorsToExceptions) { $oldErrorHandler = set_error_handler( array('PHPUnit_Util_ErrorHandler', 'handleError'), E_ALL | E_STRICT ); if ($oldErrorHandler === null) { $errorHandlerSet = true; } else { restore_error_handler(); } } $collectCodeCoverage = $this->codeCoverage !== null && !$test instanceof PHPUnit_Extensions_SeleniumTestCase && !$test instanceof PHPUnit_Framework_Warning; if ($collectCodeCoverage) { // We need to blacklist test source files when no whitelist is used. if (!$this->codeCoverage->filter()->hasWhitelist()) { $classes = $this->getHierarchy(get_class($test), true); foreach ($classes as $class) { $this->codeCoverage->filter()->addFileToBlacklist( $class->getFileName() ); } } $this->codeCoverage->start($test); } PHP_Timer::start(); try { if (!$test instanceof PHPUnit_Framework_Warning && $test->getSize() != PHPUnit_Util_Test::UNKNOWN && $this->beStrictAboutTestSize && extension_loaded('pcntl') && class_exists('PHP_Invoker')) { switch ($test->getSize()) { case PHPUnit_Util_Test::SMALL: $_timeout = $this->timeoutForSmallTests; break; case PHPUnit_Util_Test::MEDIUM: $_timeout = $this->timeoutForMediumTests; break; case PHPUnit_Util_Test::LARGE: $_timeout = $this->timeoutForLargeTests; break; } $invoker = new PHP_Invoker; $invoker->invoke(array($test, 'runBare'), array(), $_timeout); } else { $test->runBare(); } } catch (PHPUnit_Framework_AssertionFailedError $e) { $failure = true; if ($e instanceof PHPUnit_Framework_RiskyTestError) { $risky = true; } elseif ($e instanceof PHPUnit_Framework_IncompleteTestError) { $incomplete = true; } elseif ($e instanceof PHPUnit_Framework_SkippedTestError) { $skipped = true; } } catch (PHPUnit_Framework_Exception $e) { $error = true; } catch (Throwable $e) { $e = new PHPUnit_Framework_ExceptionWrapper($e); $error = true; } catch (Exception $e) { $e = new PHPUnit_Framework_ExceptionWrapper($e); $error = true; } $time = PHP_Timer::stop(); $test->addToAssertionCount(PHPUnit_Framework_Assert::getCount()); if ($this->beStrictAboutTestsThatDoNotTestAnything && $test->getNumAssertions() == 0) { $risky = true; } if ($collectCodeCoverage) { $append = !$risky && !$incomplete && !$skipped; $linesToBeCovered = array(); $linesToBeUsed = array(); if ($append && $test instanceof PHPUnit_Framework_TestCase) { $linesToBeCovered = PHPUnit_Util_Test::getLinesToBeCovered( get_class($test), $test->getName(false) ); $linesToBeUsed = PHPUnit_Util_Test::getLinesToBeUsed( get_class($test), $test->getName(false) ); } try { $this->codeCoverage->stop( $append, $linesToBeCovered, $linesToBeUsed ); } catch (PHP_CodeCoverage_Exception_UnintentionallyCoveredCode $cce) { $this->addFailure( $test, new PHPUnit_Framework_UnintentionallyCoveredCodeError( 'This test executed code that is not listed as code to be covered or used:' . PHP_EOL . $cce->getMessage() ), $time ); } catch (PHPUnit_Framework_InvalidCoversTargetException $cce) { $this->addFailure( $test, new PHPUnit_Framework_InvalidCoversTargetError( $cce->getMessage() ), $time ); } catch (PHP_CodeCoverage_Exception $cce) { $error = true; if (!isset($e)) { $e = $cce; } } } if ($errorHandlerSet === true) { restore_error_handler(); } if ($error === true) { $this->addError($test, $e, $time); } elseif ($failure === true) { $this->addFailure($test, $e, $time); } elseif ($this->beStrictAboutTestsThatDoNotTestAnything && $test->getNumAssertions() == 0) { $this->addFailure( $test, new PHPUnit_Framework_RiskyTestError( 'This test did not perform any assertions' ), $time ); } elseif ($this->beStrictAboutOutputDuringTests && $test->hasOutput()) { $this->addFailure( $test, new PHPUnit_Framework_OutputError( sprintf( 'This test printed output: %s', $test->getActualOutput() ) ), $time ); } elseif ($this->beStrictAboutTodoAnnotatedTests && $test instanceof PHPUnit_Framework_TestCase) { $annotations = $test->getAnnotations(); if (isset($annotations['method']['todo'])) { $this->addFailure( $test, new PHPUnit_Framework_RiskyTestError( 'Test method is annotated with @todo' ), $time ); } } $this->endTest($test, $time); } /** * Gets the number of run tests. * * @return int */ public function count() { return $this->runTests; } /** * Checks whether the test run should stop. * * @return bool */ public function shouldStop() { return $this->stop; } /** * Marks that the test run should stop. */ public function stop() { $this->stop = true; } /** * Returns the PHP_CodeCoverage object. * * @return PHP_CodeCoverage * * @since Method available since Release 3.5.0 */ public function getCodeCoverage() { return $this->codeCoverage; } /** * Sets the PHP_CodeCoverage object. * * @param PHP_CodeCoverage $codeCoverage * * @since Method available since Release 3.6.0 */ public function setCodeCoverage(PHP_CodeCoverage $codeCoverage) { $this->codeCoverage = $codeCoverage; } /** * Enables or disables the error-to-exception conversion. * * @param bool $flag * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.2.14 */ public function convertErrorsToExceptions($flag) { if (!is_bool($flag)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } $this->convertErrorsToExceptions = $flag; } /** * Returns the error-to-exception conversion setting. * * @return bool * * @since Method available since Release 3.4.0 */ public function getConvertErrorsToExceptions() { return $this->convertErrorsToExceptions; } /** * Enables or disables the stopping when an error occurs. * * @param bool $flag * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.5.0 */ public function stopOnError($flag) { if (!is_bool($flag)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } $this->stopOnError = $flag; } /** * Enables or disables the stopping when a failure occurs. * * @param bool $flag * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.1.0 */ public function stopOnFailure($flag) { if (!is_bool($flag)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } $this->stopOnFailure = $flag; } /** * @param bool $flag * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 4.0.0 */ public function beStrictAboutTestsThatDoNotTestAnything($flag) { if (!is_bool($flag)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } $this->beStrictAboutTestsThatDoNotTestAnything = $flag; } /** * @return bool * * @since Method available since Release 4.0.0 */ public function isStrictAboutTestsThatDoNotTestAnything() { return $this->beStrictAboutTestsThatDoNotTestAnything; } /** * @param bool $flag * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 4.0.0 */ public function beStrictAboutOutputDuringTests($flag) { if (!is_bool($flag)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } $this->beStrictAboutOutputDuringTests = $flag; } /** * @return bool * * @since Method available since Release 4.0.0 */ public function isStrictAboutOutputDuringTests() { return $this->beStrictAboutOutputDuringTests; } /** * @param bool $flag * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 4.0.0 */ public function beStrictAboutTestSize($flag) { if (!is_bool($flag)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } $this->beStrictAboutTestSize = $flag; } /** * @return bool * * @since Method available since Release 4.0.0 */ public function isStrictAboutTestSize() { return $this->beStrictAboutTestSize; } /** * @param bool $flag * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 4.2.0 */ public function beStrictAboutTodoAnnotatedTests($flag) { if (!is_bool($flag)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } $this->beStrictAboutTodoAnnotatedTests = $flag; } /** * @return bool * * @since Method available since Release 4.2.0 */ public function isStrictAboutTodoAnnotatedTests() { return $this->beStrictAboutTodoAnnotatedTests; } /** * Enables or disables the stopping for risky tests. * * @param bool $flag * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 4.0.0 */ public function stopOnRisky($flag) { if (!is_bool($flag)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } $this->stopOnRisky = $flag; } /** * Enables or disables the stopping for incomplete tests. * * @param bool $flag * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.5.0 */ public function stopOnIncomplete($flag) { if (!is_bool($flag)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } $this->stopOnIncomplete = $flag; } /** * Enables or disables the stopping for skipped tests. * * @param bool $flag * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.1.0 */ public function stopOnSkipped($flag) { if (!is_bool($flag)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } $this->stopOnSkipped = $flag; } /** * Returns the time spent running the tests. * * @return float */ public function time() { return $this->time; } /** * Returns whether the entire test was successful or not. * * @return bool */ public function wasSuccessful() { return empty($this->errors) && empty($this->failures); } /** * Sets the timeout for small tests. * * @param int $timeout * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.6.0 */ public function setTimeoutForSmallTests($timeout) { if (!is_integer($timeout)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'integer'); } $this->timeoutForSmallTests = $timeout; } /** * Sets the timeout for medium tests. * * @param int $timeout * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.6.0 */ public function setTimeoutForMediumTests($timeout) { if (!is_integer($timeout)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'integer'); } $this->timeoutForMediumTests = $timeout; } /** * Sets the timeout for large tests. * * @param int $timeout * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.6.0 */ public function setTimeoutForLargeTests($timeout) { if (!is_integer($timeout)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'integer'); } $this->timeoutForLargeTests = $timeout; } /** * Returns the class hierarchy for a given class. * * @param string $className * @param bool $asReflectionObjects * * @return array */ protected function getHierarchy($className, $asReflectionObjects = false) { if ($asReflectionObjects) { $classes = array(new ReflectionClass($className)); } else { $classes = array($className); } $done = false; while (!$done) { if ($asReflectionObjects) { $class = new ReflectionClass( $classes[count($classes) - 1]->getName() ); } else { $class = new ReflectionClass($classes[count($classes) - 1]); } $parent = $class->getParentClass(); if ($parent !== false) { if ($asReflectionObjects) { $classes[] = $parent; } else { $classes[] = $parent->getName(); } } else { $done = true; } } return $classes; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 3.4.0 */ class PHPUnit_Framework_TestSuite_DataProvider extends PHPUnit_Framework_TestSuite { /** * Sets the dependencies of a TestCase. * * @param array $dependencies */ public function setDependencies(array $dependencies) { foreach ($this->tests as $test) { $test->setDependencies($dependencies); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A TestSuite is a composite of Tests. It runs a collection of test cases. * * @since Class available since Release 2.0.0 */ class PHPUnit_Framework_TestSuite implements PHPUnit_Framework_Test, PHPUnit_Framework_SelfDescribing, IteratorAggregate { /** * Last count of tests in this suite. * * @var int|null */ private $cachedNumTests; /** * Enable or disable the backup and restoration of the $GLOBALS array. * * @var bool */ protected $backupGlobals = null; /** * Enable or disable the backup and restoration of static attributes. * * @var bool */ protected $backupStaticAttributes = null; /** * @var bool */ private $disallowChangesToGlobalState = null; /** * @var bool */ protected $runTestInSeparateProcess = false; /** * The name of the test suite. * * @var string */ protected $name = ''; /** * The test groups of the test suite. * * @var array */ protected $groups = array(); /** * The tests in the test suite. * * @var array */ protected $tests = array(); /** * The number of tests in the test suite. * * @var int */ protected $numTests = -1; /** * @var bool */ protected $testCase = false; /** * @var array */ protected $foundClasses = array(); /** * @var PHPUnit_Runner_Filter_Factory */ private $iteratorFilter = null; /** * Constructs a new TestSuite: * * - PHPUnit_Framework_TestSuite() constructs an empty TestSuite. * * - PHPUnit_Framework_TestSuite(ReflectionClass) constructs a * TestSuite from the given class. * * - PHPUnit_Framework_TestSuite(ReflectionClass, String) * constructs a TestSuite from the given class with the given * name. * * - PHPUnit_Framework_TestSuite(String) either constructs a * TestSuite from the given class (if the passed string is the * name of an existing class) or constructs an empty TestSuite * with the given name. * * @param mixed $theClass * @param string $name * * @throws PHPUnit_Framework_Exception */ public function __construct($theClass = '', $name = '') { $argumentsValid = false; if (is_object($theClass) && $theClass instanceof ReflectionClass) { $argumentsValid = true; } elseif (is_string($theClass) && $theClass !== '' && class_exists($theClass, false)) { $argumentsValid = true; if ($name == '') { $name = $theClass; } $theClass = new ReflectionClass($theClass); } elseif (is_string($theClass)) { $this->setName($theClass); return; } if (!$argumentsValid) { throw new PHPUnit_Framework_Exception; } if (!$theClass->isSubclassOf('PHPUnit_Framework_TestCase')) { throw new PHPUnit_Framework_Exception( 'Class "' . $theClass->name . '" does not extend PHPUnit_Framework_TestCase.' ); } if ($name != '') { $this->setName($name); } else { $this->setName($theClass->getName()); } $constructor = $theClass->getConstructor(); if ($constructor !== null && !$constructor->isPublic()) { $this->addTest( self::warning( sprintf( 'Class "%s" has no public constructor.', $theClass->getName() ) ) ); return; } foreach ($theClass->getMethods() as $method) { $this->addTestMethod($theClass, $method); } if (empty($this->tests)) { $this->addTest( self::warning( sprintf( 'No tests found in class "%s".', $theClass->getName() ) ) ); } $this->testCase = true; } /** * Returns a string representation of the test suite. * * @return string */ public function toString() { return $this->getName(); } /** * Adds a test to the suite. * * @param PHPUnit_Framework_Test $test * @param array $groups */ public function addTest(PHPUnit_Framework_Test $test, $groups = array()) { $class = new ReflectionClass($test); if (!$class->isAbstract()) { $this->tests[] = $test; $this->numTests = -1; if ($test instanceof self && empty($groups)) { $groups = $test->getGroups(); } if (empty($groups)) { $groups = array('default'); } foreach ($groups as $group) { if (!isset($this->groups[$group])) { $this->groups[$group] = array($test); } else { $this->groups[$group][] = $test; } } } } /** * Adds the tests from the given class to the suite. * * @param mixed $testClass * * @throws PHPUnit_Framework_Exception */ public function addTestSuite($testClass) { if (is_string($testClass) && class_exists($testClass)) { $testClass = new ReflectionClass($testClass); } if (!is_object($testClass)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 1, 'class name or object' ); } if ($testClass instanceof self) { $this->addTest($testClass); } elseif ($testClass instanceof ReflectionClass) { $suiteMethod = false; if (!$testClass->isAbstract()) { if ($testClass->hasMethod(PHPUnit_Runner_BaseTestRunner::SUITE_METHODNAME)) { $method = $testClass->getMethod( PHPUnit_Runner_BaseTestRunner::SUITE_METHODNAME ); if ($method->isStatic()) { $this->addTest( $method->invoke(null, $testClass->getName()) ); $suiteMethod = true; } } } if (!$suiteMethod && !$testClass->isAbstract()) { $this->addTest(new self($testClass)); } } else { throw new PHPUnit_Framework_Exception; } } /** * Wraps both addTest() and addTestSuite * as well as the separate import statements for the user's convenience. * * If the named file cannot be read or there are no new tests that can be * added, a PHPUnit_Framework_Warning will be created instead, * leaving the current test run untouched. * * @param string $filename * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 2.3.0 */ public function addTestFile($filename) { if (!is_string($filename)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (file_exists($filename) && substr($filename, -5) == '.phpt') { $this->addTest( new PHPUnit_Extensions_PhptTestCase($filename) ); return; } // The given file may contain further stub classes in addition to the // test class itself. Figure out the actual test class. $classes = get_declared_classes(); $filename = PHPUnit_Util_Fileloader::checkAndLoad($filename); $newClasses = array_diff(get_declared_classes(), $classes); // The diff is empty in case a parent class (with test methods) is added // AFTER a child class that inherited from it. To account for that case, // cumulate all discovered classes, so the parent class may be found in // a later invocation. if ($newClasses) { // On the assumption that test classes are defined first in files, // process discovered classes in approximate LIFO order, so as to // avoid unnecessary reflection. $this->foundClasses = array_merge($newClasses, $this->foundClasses); } // The test class's name must match the filename, either in full, or as // a PEAR/PSR-0 prefixed shortname ('NameSpace_ShortName'), or as a // PSR-1 local shortname ('NameSpace\ShortName'). The comparison must be // anchored to prevent false-positive matches (e.g., 'OtherShortName'). $shortname = basename($filename, '.php'); $shortnameRegEx = '/(?:^|_|\\\\)' . preg_quote($shortname, '/') . '$/'; foreach ($this->foundClasses as $i => $className) { if (preg_match($shortnameRegEx, $className)) { $class = new ReflectionClass($className); if ($class->getFileName() == $filename) { $newClasses = array($className); unset($this->foundClasses[$i]); break; } } } foreach ($newClasses as $className) { $class = new ReflectionClass($className); if (!$class->isAbstract()) { if ($class->hasMethod(PHPUnit_Runner_BaseTestRunner::SUITE_METHODNAME)) { $method = $class->getMethod( PHPUnit_Runner_BaseTestRunner::SUITE_METHODNAME ); if ($method->isStatic()) { $this->addTest($method->invoke(null, $className)); } } elseif ($class->implementsInterface('PHPUnit_Framework_Test')) { $this->addTestSuite($class); } } } $this->numTests = -1; } /** * Wrapper for addTestFile() that adds multiple test files. * * @param array|Iterator $filenames * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 2.3.0 */ public function addTestFiles($filenames) { if (!(is_array($filenames) || (is_object($filenames) && $filenames instanceof Iterator))) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 1, 'array or iterator' ); } foreach ($filenames as $filename) { $this->addTestFile((string) $filename); } } /** * Counts the number of test cases that will be run by this test. * * @param bool $preferCache Indicates if cache is preferred. * * @return int */ public function count($preferCache = false) { if ($preferCache && $this->cachedNumTests != null) { $numTests = $this->cachedNumTests; } else { $numTests = 0; foreach ($this as $test) { $numTests += count($test); } $this->cachedNumTests = $numTests; } return $numTests; } /** * @param ReflectionClass $theClass * @param string $name * * @return PHPUnit_Framework_Test * * @throws PHPUnit_Framework_Exception */ public static function createTest(ReflectionClass $theClass, $name) { $className = $theClass->getName(); if (!$theClass->isInstantiable()) { return self::warning( sprintf('Cannot instantiate class "%s".', $className) ); } $backupSettings = PHPUnit_Util_Test::getBackupSettings( $className, $name ); $preserveGlobalState = PHPUnit_Util_Test::getPreserveGlobalStateSettings( $className, $name ); $runTestInSeparateProcess = PHPUnit_Util_Test::getProcessIsolationSettings( $className, $name ); $constructor = $theClass->getConstructor(); if ($constructor !== null) { $parameters = $constructor->getParameters(); // TestCase() or TestCase($name) if (count($parameters) < 2) { $test = new $className; } // TestCase($name, $data) else { try { $data = PHPUnit_Util_Test::getProvidedData( $className, $name ); } catch (PHPUnit_Framework_IncompleteTestError $e) { $message = sprintf( 'Test for %s::%s marked incomplete by data provider', $className, $name ); $_message = $e->getMessage(); if (!empty($_message)) { $message .= "\n" . $_message; } $data = self::incompleteTest($className, $name, $message); } catch (PHPUnit_Framework_SkippedTestError $e) { $message = sprintf( 'Test for %s::%s skipped by data provider', $className, $name ); $_message = $e->getMessage(); if (!empty($_message)) { $message .= "\n" . $_message; } $data = self::skipTest($className, $name, $message); } catch (Throwable $_t) { $t = $_t; } catch (Exception $_t) { $t = $_t; } if (isset($t)) { $message = sprintf( 'The data provider specified for %s::%s is invalid.', $className, $name ); $_message = $t->getMessage(); if (!empty($_message)) { $message .= "\n" . $_message; } $data = self::warning($message); } // Test method with @dataProvider. if (isset($data)) { $test = new PHPUnit_Framework_TestSuite_DataProvider( $className . '::' . $name ); if (empty($data)) { $data = self::warning( sprintf( 'No tests found in suite "%s".', $test->getName() ) ); } $groups = PHPUnit_Util_Test::getGroups($className, $name); if ($data instanceof PHPUnit_Framework_Warning || $data instanceof PHPUnit_Framework_SkippedTestCase || $data instanceof PHPUnit_Framework_IncompleteTestCase) { $test->addTest($data, $groups); } else { foreach ($data as $_dataName => $_data) { $_test = new $className($name, $_data, $_dataName); if ($runTestInSeparateProcess) { $_test->setRunTestInSeparateProcess(true); if ($preserveGlobalState !== null) { $_test->setPreserveGlobalState($preserveGlobalState); } } if ($backupSettings['backupGlobals'] !== null) { $_test->setBackupGlobals( $backupSettings['backupGlobals'] ); } if ($backupSettings['backupStaticAttributes'] !== null) { $_test->setBackupStaticAttributes( $backupSettings['backupStaticAttributes'] ); } $test->addTest($_test, $groups); } } } else { $test = new $className; } } } if (!isset($test)) { throw new PHPUnit_Framework_Exception('No valid test provided.'); } if ($test instanceof PHPUnit_Framework_TestCase) { $test->setName($name); if ($runTestInSeparateProcess) { $test->setRunTestInSeparateProcess(true); if ($preserveGlobalState !== null) { $test->setPreserveGlobalState($preserveGlobalState); } } if ($backupSettings['backupGlobals'] !== null) { $test->setBackupGlobals($backupSettings['backupGlobals']); } if ($backupSettings['backupStaticAttributes'] !== null) { $test->setBackupStaticAttributes( $backupSettings['backupStaticAttributes'] ); } } return $test; } /** * Creates a default TestResult object. * * @return PHPUnit_Framework_TestResult */ protected function createResult() { return new PHPUnit_Framework_TestResult; } /** * Returns the name of the suite. * * @return string */ public function getName() { return $this->name; } /** * Returns the test groups of the suite. * * @return array * * @since Method available since Release 3.2.0 */ public function getGroups() { return array_keys($this->groups); } public function getGroupDetails() { return $this->groups; } /** * Set tests groups of the test case * * @param array $groups * * @since Method available since Release 4.0.0 */ public function setGroupDetails(array $groups) { $this->groups = $groups; } /** * Runs the tests and collects their result in a TestResult. * * @param PHPUnit_Framework_TestResult $result * * @return PHPUnit_Framework_TestResult */ public function run(PHPUnit_Framework_TestResult $result = null) { if ($result === null) { $result = $this->createResult(); } if (count($this) == 0) { return $result; } $hookMethods = PHPUnit_Util_Test::getHookMethods($this->name); $result->startTestSuite($this); try { $this->setUp(); foreach ($hookMethods['beforeClass'] as $beforeClassMethod) { if ($this->testCase === true && class_exists($this->name, false) && method_exists($this->name, $beforeClassMethod)) { if ($missingRequirements = PHPUnit_Util_Test::getMissingRequirements($this->name, $beforeClassMethod)) { $this->markTestSuiteSkipped(implode(PHP_EOL, $missingRequirements)); } call_user_func(array($this->name, $beforeClassMethod)); } } } catch (PHPUnit_Framework_SkippedTestSuiteError $e) { $numTests = count($this); for ($i = 0; $i < $numTests; $i++) { $result->startTest($this); $result->addFailure($this, $e, 0); $result->endTest($this, 0); } $this->tearDown(); $result->endTestSuite($this); return $result; } catch (Throwable $_t) { $t = $_t; } catch (Exception $_t) { $t = $_t; } if (isset($t)) { $numTests = count($this); for ($i = 0; $i < $numTests; $i++) { $result->startTest($this); $result->addError($this, $t, 0); $result->endTest($this, 0); } $this->tearDown(); $result->endTestSuite($this); return $result; } foreach ($this as $test) { if ($result->shouldStop()) { break; } if ($test instanceof PHPUnit_Framework_TestCase || $test instanceof self) { $test->setDisallowChangesToGlobalState($this->disallowChangesToGlobalState); $test->setBackupGlobals($this->backupGlobals); $test->setBackupStaticAttributes($this->backupStaticAttributes); $test->setRunTestInSeparateProcess($this->runTestInSeparateProcess); } $test->run($result); } foreach ($hookMethods['afterClass'] as $afterClassMethod) { if ($this->testCase === true && class_exists($this->name, false) && method_exists($this->name, $afterClassMethod)) { call_user_func(array($this->name, $afterClassMethod)); } } $this->tearDown(); $result->endTestSuite($this); return $result; } /** * @param bool $runTestInSeparateProcess * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.7.0 */ public function setRunTestInSeparateProcess($runTestInSeparateProcess) { if (is_bool($runTestInSeparateProcess)) { $this->runTestInSeparateProcess = $runTestInSeparateProcess; } else { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } } /** * Runs a test. * * @deprecated * * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_TestResult $result */ public function runTest(PHPUnit_Framework_Test $test, PHPUnit_Framework_TestResult $result) { $test->run($result); } /** * Sets the name of the suite. * * @param string */ public function setName($name) { $this->name = $name; } /** * Returns the test at the given index. * * @param int * * @return PHPUnit_Framework_Test */ public function testAt($index) { if (isset($this->tests[$index])) { return $this->tests[$index]; } else { return false; } } /** * Returns the tests as an enumeration. * * @return array */ public function tests() { return $this->tests; } /** * Set tests of the test suite * * @param array $tests * * @since Method available since Release 4.0.0 */ public function setTests(array $tests) { $this->tests = $tests; } /** * Mark the test suite as skipped. * * @param string $message * * @throws PHPUnit_Framework_SkippedTestSuiteError * * @since Method available since Release 3.0.0 */ public function markTestSuiteSkipped($message = '') { throw new PHPUnit_Framework_SkippedTestSuiteError($message); } /** * @param ReflectionClass $class * @param ReflectionMethod $method */ protected function addTestMethod(ReflectionClass $class, ReflectionMethod $method) { if (!$this->isTestMethod($method)) { return; } $name = $method->getName(); if (!$method->isPublic()) { $this->addTest( self::warning( sprintf( 'Test method "%s" in test class "%s" is not public.', $name, $class->getName() ) ) ); return; } $test = self::createTest($class, $name); if ($test instanceof PHPUnit_Framework_TestCase || $test instanceof PHPUnit_Framework_TestSuite_DataProvider) { $test->setDependencies( PHPUnit_Util_Test::getDependencies($class->getName(), $name) ); } $this->addTest( $test, PHPUnit_Util_Test::getGroups($class->getName(), $name) ); } /** * @param ReflectionMethod $method * * @return bool */ public static function isTestMethod(ReflectionMethod $method) { if (strpos($method->name, 'test') === 0) { return true; } // @scenario on TestCase::testMethod() // @test on TestCase::testMethod() $doc_comment = $method->getDocComment(); return strpos($doc_comment, '@test') !== false || strpos($doc_comment, '@scenario') !== false; } /** * @param string $message * * @return PHPUnit_Framework_Warning */ protected static function warning($message) { return new PHPUnit_Framework_Warning($message); } /** * @param string $class * @param string $methodName * @param string $message * * @return PHPUnit_Framework_SkippedTestCase * * @since Method available since Release 4.3.0 */ protected static function skipTest($class, $methodName, $message) { return new PHPUnit_Framework_SkippedTestCase($class, $methodName, $message); } /** * @param string $class * @param string $methodName * @param string $message * * @return PHPUnit_Framework_IncompleteTestCase * * @since Method available since Release 4.3.0 */ protected static function incompleteTest($class, $methodName, $message) { return new PHPUnit_Framework_IncompleteTestCase($class, $methodName, $message); } /** * @param bool $disallowChangesToGlobalState * * @since Method available since Release 4.6.0 */ public function setDisallowChangesToGlobalState($disallowChangesToGlobalState) { if (is_null($this->disallowChangesToGlobalState) && is_bool($disallowChangesToGlobalState)) { $this->disallowChangesToGlobalState = $disallowChangesToGlobalState; } } /** * @param bool $backupGlobals * * @since Method available since Release 3.3.0 */ public function setBackupGlobals($backupGlobals) { if (is_null($this->backupGlobals) && is_bool($backupGlobals)) { $this->backupGlobals = $backupGlobals; } } /** * @param bool $backupStaticAttributes * * @since Method available since Release 3.4.0 */ public function setBackupStaticAttributes($backupStaticAttributes) { if (is_null($this->backupStaticAttributes) && is_bool($backupStaticAttributes)) { $this->backupStaticAttributes = $backupStaticAttributes; } } /** * Returns an iterator for this test suite. * * @return RecursiveIteratorIterator * * @since Method available since Release 3.1.0 */ public function getIterator() { $iterator = new PHPUnit_Util_TestSuiteIterator($this); if ($this->iteratorFilter !== null) { $iterator = $this->iteratorFilter->factory($iterator, $this); } return $iterator; } public function injectFilter(PHPUnit_Runner_Filter_Factory $filter) { $this->iteratorFilter = $filter; foreach ($this as $test) { if ($test instanceof self) { $test->injectFilter($filter); } } } /** * Template Method that is called before the tests * of this test suite are run. * * @since Method available since Release 3.1.0 */ protected function setUp() { } /** * Template Method that is called after the tests * of this test suite have finished running. * * @since Method available since Release 3.1.0 */ protected function tearDown() { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Extension to PHPUnit_Framework_AssertionFailedError to mark the special * case of a test test that unintentionally covers code. * * @since Class available since Release 4.0.0 */ class PHPUnit_Framework_UnintentionallyCoveredCodeError extends PHPUnit_Framework_RiskyTestError { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A warning. * * @since Class available since Release 2.0.0 */ class PHPUnit_Framework_Warning extends PHPUnit_Framework_TestCase { /** * @var string */ protected $message = ''; /** * @var bool */ protected $backupGlobals = false; /** * @var bool */ protected $backupStaticAttributes = false; /** * @var bool */ protected $runTestInSeparateProcess = false; /** * @var bool */ protected $useErrorHandler = false; /** * @param string $message */ public function __construct($message = '') { $this->message = $message; parent::__construct('Warning'); } /** * @throws PHPUnit_Framework_Exception */ protected function runTest() { $this->fail($this->message); } /** * @return string * * @since Method available since Release 3.0.0 */ public function getMessage() { return $this->message; } /** * Returns a string representation of the test case. * * @return string * * @since Method available since Release 3.4.0 */ public function toString() { return 'Warning'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Base class for all test runners. * * @since Class available since Release 2.0.0 */ abstract class PHPUnit_Runner_BaseTestRunner { const STATUS_PASSED = 0; const STATUS_SKIPPED = 1; const STATUS_INCOMPLETE = 2; const STATUS_FAILURE = 3; const STATUS_ERROR = 4; const STATUS_RISKY = 5; const SUITE_METHODNAME = 'suite'; /** * Returns the loader to be used. * * @return PHPUnit_Runner_TestSuiteLoader */ public function getLoader() { return new PHPUnit_Runner_StandardTestSuiteLoader; } /** * Returns the Test corresponding to the given suite. * This is a template method, subclasses override * the runFailed() and clearStatus() methods. * * @param string $suiteClassName * @param string $suiteClassFile * @param mixed $suffixes * * @return PHPUnit_Framework_Test */ public function getTest($suiteClassName, $suiteClassFile = '', $suffixes = '') { if (is_dir($suiteClassName) && !is_file($suiteClassName . '.php') && empty($suiteClassFile)) { $facade = new File_Iterator_Facade; $files = $facade->getFilesAsArray( $suiteClassName, $suffixes ); $suite = new PHPUnit_Framework_TestSuite($suiteClassName); $suite->addTestFiles($files); return $suite; } try { $testClass = $this->loadSuiteClass( $suiteClassName, $suiteClassFile ); } catch (PHPUnit_Framework_Exception $e) { $this->runFailed($e->getMessage()); return; } try { $suiteMethod = $testClass->getMethod(self::SUITE_METHODNAME); if (!$suiteMethod->isStatic()) { $this->runFailed( 'suite() method must be static.' ); return; } try { $test = $suiteMethod->invoke(null, $testClass->getName()); } catch (ReflectionException $e) { $this->runFailed( sprintf( "Failed to invoke suite() method.\n%s", $e->getMessage() ) ); return; } } catch (ReflectionException $e) { try { $test = new PHPUnit_Framework_TestSuite($testClass); } catch (PHPUnit_Framework_Exception $e) { $test = new PHPUnit_Framework_TestSuite; $test->setName($suiteClassName); } } $this->clearStatus(); return $test; } /** * Returns the loaded ReflectionClass for a suite name. * * @param string $suiteClassName * @param string $suiteClassFile * * @return ReflectionClass */ protected function loadSuiteClass($suiteClassName, $suiteClassFile = '') { $loader = $this->getLoader(); return $loader->load($suiteClassName, $suiteClassFile); } /** * Clears the status message. */ protected function clearStatus() { } /** * Override to define how to handle a failed loading of * a test suite. * * @param string $message */ abstract protected function runFailed($message); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 4.0.0 */ class PHPUnit_Runner_Exception extends RuntimeException implements PHPUnit_Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 4.0.0 */ class PHPUnit_Runner_Filter_Factory { /** * @var array */ private $filters = array(); /** * @param ReflectionClass $filter * @param mixed $args */ public function addFilter(ReflectionClass $filter, $args) { if (!$filter->isSubclassOf('RecursiveFilterIterator')) { throw new InvalidArgumentException( sprintf( 'Class "%s" does not extend RecursiveFilterIterator', $filter->name ) ); } $this->filters[] = array($filter, $args); } /** * @return FilterIterator */ public function factory(Iterator $iterator, PHPUnit_Framework_TestSuite $suite) { foreach ($this->filters as $filter) { list($class, $args) = $filter; $iterator = $class->newInstance($iterator, $args, $suite); } return $iterator; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 4.0.0 */ class PHPUnit_Runner_Filter_Group_Exclude extends PHPUnit_Runner_Filter_GroupFilterIterator { protected function doAccept($hash) { return !in_array($hash, $this->groupTests); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 4.0.0 */ class PHPUnit_Runner_Filter_Group_Include extends PHPUnit_Runner_Filter_GroupFilterIterator { protected function doAccept($hash) { return in_array($hash, $this->groupTests); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 4.0.0 */ abstract class PHPUnit_Runner_Filter_GroupFilterIterator extends RecursiveFilterIterator { /** * @var array */ protected $groupTests = array(); /** * @param RecursiveIterator $iterator * @param array $groups * @param PHPUnit_Framework_TestSuite $suite */ public function __construct(RecursiveIterator $iterator, array $groups, PHPUnit_Framework_TestSuite $suite) { parent::__construct($iterator); foreach ($suite->getGroupDetails() as $group => $tests) { if (in_array($group, $groups)) { $testHashes = array_map( function ($test) { return spl_object_hash($test); }, $tests ); $this->groupTests = array_merge($this->groupTests, $testHashes); } } } /** * @return bool */ public function accept() { $test = $this->getInnerIterator()->current(); if ($test instanceof PHPUnit_Framework_TestSuite) { return true; } return $this->doAccept(spl_object_hash($test)); } abstract protected function doAccept($hash); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 4.0.0 */ class PHPUnit_Runner_Filter_Test extends RecursiveFilterIterator { /** * @var string */ protected $filter = null; /** * @var int */ protected $filterMin; /** * @var int */ protected $filterMax; /** * @param RecursiveIterator $iterator * @param string $filter */ public function __construct(RecursiveIterator $iterator, $filter) { parent::__construct($iterator); $this->setFilter($filter); } /** * @param string $filter */ protected function setFilter($filter) { if (PHPUnit_Util_Regex::pregMatchSafe($filter, '') === false) { // Handles: // * testAssertEqualsSucceeds#4 // * testAssertEqualsSucceeds#4-8 if (preg_match('/^(.*?)#(\d+)(?:-(\d+))?$/', $filter, $matches)) { if (isset($matches[3]) && $matches[2] < $matches[3]) { $filter = sprintf( '%s.*with data set #(\d+)$', $matches[1] ); $this->filterMin = $matches[2]; $this->filterMax = $matches[3]; } else { $filter = sprintf( '%s.*with data set #%s$', $matches[1], $matches[2] ); } } // Handles: // * testDetermineJsonError@JSON_ERROR_NONE // * testDetermineJsonError@JSON.* elseif (preg_match('/^(.*?)@(.+)$/', $filter, $matches)) { $filter = sprintf( '%s.*with data set "%s"$', $matches[1], $matches[2] ); } // Escape delimiters in regular expression. Do NOT use preg_quote, // to keep magic characters. $filter = sprintf('/%s/', str_replace( '/', '\\/', $filter )); } $this->filter = $filter; } /** * @return bool */ public function accept() { $test = $this->getInnerIterator()->current(); if ($test instanceof PHPUnit_Framework_TestSuite) { return true; } $tmp = PHPUnit_Util_Test::describe($test, false); if ($tmp[0] != '') { $name = implode('::', $tmp); } else { $name = $tmp[1]; } $accepted = @preg_match($this->filter, $name, $matches); if ($accepted && isset($this->filterMax)) { $set = end($matches); $accepted = $set >= $this->filterMin && $set <= $this->filterMax; } return $accepted; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * The standard test suite loader. * * @since Class available since Release 2.0.0 */ class PHPUnit_Runner_StandardTestSuiteLoader implements PHPUnit_Runner_TestSuiteLoader { /** * @param string $suiteClassName * @param string $suiteClassFile * * @return ReflectionClass * * @throws PHPUnit_Framework_Exception */ public function load($suiteClassName, $suiteClassFile = '') { $suiteClassName = str_replace('.php', '', $suiteClassName); if (empty($suiteClassFile)) { $suiteClassFile = PHPUnit_Util_Filesystem::classNameToFilename( $suiteClassName ); } if (!class_exists($suiteClassName, false)) { $loadedClasses = get_declared_classes(); $filename = PHPUnit_Util_Fileloader::checkAndLoad($suiteClassFile); $loadedClasses = array_values( array_diff(get_declared_classes(), $loadedClasses) ); } if (!class_exists($suiteClassName, false) && !empty($loadedClasses)) { $offset = 0 - strlen($suiteClassName); foreach ($loadedClasses as $loadedClass) { $class = new ReflectionClass($loadedClass); if (substr($loadedClass, $offset) === $suiteClassName && $class->getFileName() == $filename) { $suiteClassName = $loadedClass; break; } } } if (!class_exists($suiteClassName, false) && !empty($loadedClasses)) { $testCaseClass = 'PHPUnit_Framework_TestCase'; foreach ($loadedClasses as $loadedClass) { $class = new ReflectionClass($loadedClass); $classFile = $class->getFileName(); if ($class->isSubclassOf($testCaseClass) && !$class->isAbstract()) { $suiteClassName = $loadedClass; $testCaseClass = $loadedClass; if ($classFile == realpath($suiteClassFile)) { break; } } if ($class->hasMethod('suite')) { $method = $class->getMethod('suite'); if (!$method->isAbstract() && $method->isPublic() && $method->isStatic()) { $suiteClassName = $loadedClass; if ($classFile == realpath($suiteClassFile)) { break; } } } } } if (class_exists($suiteClassName, false)) { $class = new ReflectionClass($suiteClassName); if ($class->getFileName() == realpath($suiteClassFile)) { return $class; } } throw new PHPUnit_Framework_Exception( sprintf( "Class '%s' could not be found in '%s'.", $suiteClassName, $suiteClassFile ) ); } /** * @param ReflectionClass $aClass * * @return ReflectionClass */ public function reload(ReflectionClass $aClass) { return $aClass; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * An interface to define how a test suite should be loaded. * * @since Interface available since Release 2.0.0 */ interface PHPUnit_Runner_TestSuiteLoader { /** * @param string $suiteClassName * @param string $suiteClassFile * * @return ReflectionClass */ public function load($suiteClassName, $suiteClassFile = ''); /** * @param ReflectionClass $aClass * * @return ReflectionClass */ public function reload(ReflectionClass $aClass); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * This class defines the current version of PHPUnit. * * @since Class available since Release 2.0.0 */ class PHPUnit_Runner_Version { private static $pharVersion; private static $version; /** * Returns the current version of PHPUnit. * * @return string */ public static function id() { if (self::$pharVersion !== null) { return self::$pharVersion; } if (self::$version === null) { $version = new SebastianBergmann\Version('4.8.31', dirname(dirname(__DIR__))); self::$version = $version->getVersion(); } return self::$version; } /** * @return string * * @since Method available since Release 4.8.13 */ public static function series() { if (strpos(self::id(), '-')) { $tmp = explode('-', self::id()); $version = $tmp[0]; } else { $version = self::id(); } return implode('.', array_slice(explode('.', $version), 0, 2)); } /** * @return string */ public static function getVersionString() { return 'PHPUnit ' . self::id() . ' by Sebastian Bergmann and contributors.'; } /** * @return string * * @since Method available since Release 4.0.0 */ public static function getReleaseChannel() { if (strpos(self::$pharVersion, '-') !== false) { return '-nightly'; } return ''; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A TestRunner for the Command Line Interface (CLI) * PHP SAPI Module. * * @since Class available since Release 3.0.0 */ class PHPUnit_TextUI_Command { /** * @var array */ protected $arguments = array( 'listGroups' => false, 'loader' => null, 'useDefaultConfiguration' => true ); /** * @var array */ protected $options = array(); /** * @var array */ protected $longOptions = array( 'colors==' => null, 'bootstrap=' => null, 'columns=' => null, 'configuration=' => null, 'coverage-clover=' => null, 'coverage-crap4j=' => null, 'coverage-html=' => null, 'coverage-php=' => null, 'coverage-text==' => null, 'coverage-xml=' => null, 'debug' => null, 'exclude-group=' => null, 'filter=' => null, 'testsuite=' => null, 'group=' => null, 'help' => null, 'include-path=' => null, 'list-groups' => null, 'loader=' => null, 'log-json=' => null, 'log-junit=' => null, 'log-tap=' => null, 'process-isolation' => null, 'repeat=' => null, 'stderr' => null, 'stop-on-error' => null, 'stop-on-failure' => null, 'stop-on-incomplete' => null, 'stop-on-risky' => null, 'stop-on-skipped' => null, 'report-useless-tests' => null, 'strict-coverage' => null, 'disallow-test-output' => null, 'enforce-time-limit' => null, 'disallow-todo-tests' => null, 'strict-global-state' => null, 'strict' => null, 'tap' => null, 'testdox' => null, 'testdox-html=' => null, 'testdox-text=' => null, 'test-suffix=' => null, 'no-configuration' => null, 'no-coverage' => null, 'no-globals-backup' => null, 'printer=' => null, 'static-backup' => null, 'verbose' => null, 'version' => null ); /** * @var bool */ private $versionStringPrinted = false; /** * @param bool $exit */ public static function main($exit = true) { $command = new static; return $command->run($_SERVER['argv'], $exit); } /** * @param array $argv * @param bool $exit * * @return int */ public function run(array $argv, $exit = true) { $this->handleArguments($argv); $runner = $this->createRunner(); if (is_object($this->arguments['test']) && $this->arguments['test'] instanceof PHPUnit_Framework_Test) { $suite = $this->arguments['test']; } else { $suite = $runner->getTest( $this->arguments['test'], $this->arguments['testFile'], $this->arguments['testSuffixes'] ); } if ($this->arguments['listGroups']) { $this->printVersionString(); print "Available test group(s):\n"; $groups = $suite->getGroups(); sort($groups); foreach ($groups as $group) { print " - $group\n"; } if ($exit) { exit(PHPUnit_TextUI_TestRunner::SUCCESS_EXIT); } else { return PHPUnit_TextUI_TestRunner::SUCCESS_EXIT; } } unset($this->arguments['test']); unset($this->arguments['testFile']); try { $result = $runner->doRun($suite, $this->arguments); } catch (PHPUnit_Framework_Exception $e) { print $e->getMessage() . "\n"; } $ret = PHPUnit_TextUI_TestRunner::FAILURE_EXIT; if (isset($result) && $result->wasSuccessful()) { $ret = PHPUnit_TextUI_TestRunner::SUCCESS_EXIT; } elseif (!isset($result) || $result->errorCount() > 0) { $ret = PHPUnit_TextUI_TestRunner::EXCEPTION_EXIT; } if ($exit) { exit($ret); } else { return $ret; } } /** * Create a TestRunner, override in subclasses. * * @return PHPUnit_TextUI_TestRunner * * @since Method available since Release 3.6.0 */ protected function createRunner() { return new PHPUnit_TextUI_TestRunner($this->arguments['loader']); } /** * Handles the command-line arguments. * * A child class of PHPUnit_TextUI_Command can hook into the argument * parsing by adding the switch(es) to the $longOptions array and point to a * callback method that handles the switch(es) in the child class like this * * * longOptions['my-switch'] = 'myHandler'; * // my-secondswitch will accept a value - note the equals sign * $this->longOptions['my-secondswitch='] = 'myOtherHandler'; * } * * // --my-switch -> myHandler() * protected function myHandler() * { * } * * // --my-secondswitch foo -> myOtherHandler('foo') * protected function myOtherHandler ($value) * { * } * * // You will also need this - the static keyword in the * // PHPUnit_TextUI_Command will mean that it'll be * // PHPUnit_TextUI_Command that gets instantiated, * // not MyCommand * public static function main($exit = true) * { * $command = new static; * * return $command->run($_SERVER['argv'], $exit); * } * * } * * * @param array $argv */ protected function handleArguments(array $argv) { if (defined('__PHPUNIT_PHAR__')) { $this->longOptions['check-version'] = null; $this->longOptions['selfupdate'] = null; $this->longOptions['self-update'] = null; $this->longOptions['selfupgrade'] = null; $this->longOptions['self-upgrade'] = null; } try { $this->options = PHPUnit_Util_Getopt::getopt( $argv, 'd:c:hv', array_keys($this->longOptions) ); } catch (PHPUnit_Framework_Exception $e) { $this->showError($e->getMessage()); } foreach ($this->options[0] as $option) { switch ($option[0]) { case '--colors': $this->arguments['colors'] = $option[1] ?: PHPUnit_TextUI_ResultPrinter::COLOR_AUTO; break; case '--bootstrap': $this->arguments['bootstrap'] = $option[1]; break; case '--columns': if (is_numeric($option[1])) { $this->arguments['columns'] = (int) $option[1]; } elseif ($option[1] == 'max') { $this->arguments['columns'] = 'max'; } break; case 'c': case '--configuration': $this->arguments['configuration'] = $option[1]; break; case '--coverage-clover': $this->arguments['coverageClover'] = $option[1]; break; case '--coverage-crap4j': $this->arguments['coverageCrap4J'] = $option[1]; break; case '--coverage-html': $this->arguments['coverageHtml'] = $option[1]; break; case '--coverage-php': $this->arguments['coveragePHP'] = $option[1]; break; case '--coverage-text': if ($option[1] === null) { $option[1] = 'php://stdout'; } $this->arguments['coverageText'] = $option[1]; $this->arguments['coverageTextShowUncoveredFiles'] = false; $this->arguments['coverageTextShowOnlySummary'] = false; break; case '--coverage-xml': $this->arguments['coverageXml'] = $option[1]; break; case 'd': $ini = explode('=', $option[1]); if (isset($ini[0])) { if (isset($ini[1])) { ini_set($ini[0], $ini[1]); } else { ini_set($ini[0], true); } } break; case '--debug': $this->arguments['debug'] = true; break; case 'h': case '--help': $this->showHelp(); exit(PHPUnit_TextUI_TestRunner::SUCCESS_EXIT); break; case '--filter': $this->arguments['filter'] = $option[1]; break; case '--testsuite': $this->arguments['testsuite'] = $option[1]; break; case '--group': $this->arguments['groups'] = explode(',', $option[1]); break; case '--exclude-group': $this->arguments['excludeGroups'] = explode( ',', $option[1] ); break; case '--test-suffix': $this->arguments['testSuffixes'] = explode( ',', $option[1] ); break; case '--include-path': $includePath = $option[1]; break; case '--list-groups': $this->arguments['listGroups'] = true; break; case '--printer': $this->arguments['printer'] = $option[1]; break; case '--loader': $this->arguments['loader'] = $option[1]; break; case '--log-json': $this->arguments['jsonLogfile'] = $option[1]; break; case '--log-junit': $this->arguments['junitLogfile'] = $option[1]; break; case '--log-tap': $this->arguments['tapLogfile'] = $option[1]; break; case '--process-isolation': $this->arguments['processIsolation'] = true; break; case '--repeat': $this->arguments['repeat'] = (int) $option[1]; break; case '--stderr': $this->arguments['stderr'] = true; break; case '--stop-on-error': $this->arguments['stopOnError'] = true; break; case '--stop-on-failure': $this->arguments['stopOnFailure'] = true; break; case '--stop-on-incomplete': $this->arguments['stopOnIncomplete'] = true; break; case '--stop-on-risky': $this->arguments['stopOnRisky'] = true; break; case '--stop-on-skipped': $this->arguments['stopOnSkipped'] = true; break; case '--tap': $this->arguments['printer'] = 'PHPUnit_Util_Log_TAP'; break; case '--testdox': $this->arguments['printer'] = 'PHPUnit_Util_TestDox_ResultPrinter_Text'; break; case '--testdox-html': $this->arguments['testdoxHTMLFile'] = $option[1]; break; case '--testdox-text': $this->arguments['testdoxTextFile'] = $option[1]; break; case '--no-configuration': $this->arguments['useDefaultConfiguration'] = false; break; case '--no-coverage': $this->arguments['noCoverage'] = true; break; case '--no-globals-backup': $this->arguments['backupGlobals'] = false; break; case '--static-backup': $this->arguments['backupStaticAttributes'] = true; break; case 'v': case '--verbose': $this->arguments['verbose'] = true; break; case '--version': $this->printVersionString(); exit(PHPUnit_TextUI_TestRunner::SUCCESS_EXIT); break; case '--report-useless-tests': $this->arguments['reportUselessTests'] = true; break; case '--strict-coverage': $this->arguments['strictCoverage'] = true; break; case '--strict-global-state': $this->arguments['disallowChangesToGlobalState'] = true; break; case '--disallow-test-output': $this->arguments['disallowTestOutput'] = true; break; case '--enforce-time-limit': $this->arguments['enforceTimeLimit'] = true; break; case '--disallow-todo-tests': $this->arguments['disallowTodoAnnotatedTests'] = true; break; case '--strict': $this->arguments['reportUselessTests'] = true; $this->arguments['strictCoverage'] = true; $this->arguments['disallowTestOutput'] = true; $this->arguments['enforceTimeLimit'] = true; $this->arguments['disallowTodoAnnotatedTests'] = true; $this->arguments['deprecatedStrictModeOption'] = true; break; case '--check-version': $this->handleVersionCheck(); break; case '--selfupdate': case '--self-update': $this->handleSelfUpdate(); break; case '--selfupgrade': case '--self-upgrade': $this->handleSelfUpdate(true); break; case '--whitelist': $this->arguments['whitelist'] = $option[1]; break; default: $optionName = str_replace('--', '', $option[0]); if (isset($this->longOptions[$optionName])) { $handler = $this->longOptions[$optionName]; } elseif (isset($this->longOptions[$optionName . '='])) { $handler = $this->longOptions[$optionName . '=']; } if (isset($handler) && is_callable(array($this, $handler))) { $this->$handler($option[1]); } } } $this->handleCustomTestSuite(); if (!isset($this->arguments['test'])) { if (isset($this->options[1][0])) { $this->arguments['test'] = $this->options[1][0]; } if (isset($this->options[1][1])) { $this->arguments['testFile'] = realpath($this->options[1][1]); } else { $this->arguments['testFile'] = ''; } if (isset($this->arguments['test']) && is_file($this->arguments['test']) && substr($this->arguments['test'], -5, 5) != '.phpt') { $this->arguments['testFile'] = realpath($this->arguments['test']); $this->arguments['test'] = substr($this->arguments['test'], 0, strrpos($this->arguments['test'], '.')); } } if (!isset($this->arguments['testSuffixes'])) { $this->arguments['testSuffixes'] = array('Test.php', '.phpt'); } if (isset($includePath)) { ini_set( 'include_path', $includePath . PATH_SEPARATOR . ini_get('include_path') ); } if ($this->arguments['loader'] !== null) { $this->arguments['loader'] = $this->handleLoader($this->arguments['loader']); } if (isset($this->arguments['configuration']) && is_dir($this->arguments['configuration'])) { $configurationFile = $this->arguments['configuration'] . '/phpunit.xml'; if (file_exists($configurationFile)) { $this->arguments['configuration'] = realpath( $configurationFile ); } elseif (file_exists($configurationFile . '.dist')) { $this->arguments['configuration'] = realpath( $configurationFile . '.dist' ); } } elseif (!isset($this->arguments['configuration']) && $this->arguments['useDefaultConfiguration']) { if (file_exists('phpunit.xml')) { $this->arguments['configuration'] = realpath('phpunit.xml'); } elseif (file_exists('phpunit.xml.dist')) { $this->arguments['configuration'] = realpath( 'phpunit.xml.dist' ); } } if (isset($this->arguments['configuration'])) { try { $configuration = PHPUnit_Util_Configuration::getInstance( $this->arguments['configuration'] ); } catch (Throwable $e) { print $e->getMessage() . "\n"; exit(PHPUnit_TextUI_TestRunner::FAILURE_EXIT); } catch (Exception $e) { print $e->getMessage() . "\n"; exit(PHPUnit_TextUI_TestRunner::FAILURE_EXIT); } $phpunit = $configuration->getPHPUnitConfiguration(); $configuration->handlePHPConfiguration(); /* * Issue #1216 */ if (isset($this->arguments['bootstrap'])) { $this->handleBootstrap($this->arguments['bootstrap']); } elseif (isset($phpunit['bootstrap'])) { $this->handleBootstrap($phpunit['bootstrap']); } /* * Issue #657 */ if (isset($phpunit['stderr']) && ! isset($this->arguments['stderr'])) { $this->arguments['stderr'] = $phpunit['stderr']; } if (isset($phpunit['columns']) && ! isset($this->arguments['columns'])) { $this->arguments['columns'] = $phpunit['columns']; } if (isset($phpunit['printerClass'])) { if (isset($phpunit['printerFile'])) { $file = $phpunit['printerFile']; } else { $file = ''; } $this->arguments['printer'] = $this->handlePrinter( $phpunit['printerClass'], $file ); } if (isset($phpunit['testSuiteLoaderClass'])) { if (isset($phpunit['testSuiteLoaderFile'])) { $file = $phpunit['testSuiteLoaderFile']; } else { $file = ''; } $this->arguments['loader'] = $this->handleLoader( $phpunit['testSuiteLoaderClass'], $file ); } $browsers = $configuration->getSeleniumBrowserConfiguration(); if (!empty($browsers)) { $this->arguments['deprecatedSeleniumConfiguration'] = true; if (class_exists('PHPUnit_Extensions_SeleniumTestCase')) { PHPUnit_Extensions_SeleniumTestCase::$browsers = $browsers; } } if (!isset($this->arguments['test'])) { $testSuite = $configuration->getTestSuiteConfiguration(isset($this->arguments['testsuite']) ? $this->arguments['testsuite'] : null); if ($testSuite !== null) { $this->arguments['test'] = $testSuite; } } } elseif (isset($this->arguments['bootstrap'])) { $this->handleBootstrap($this->arguments['bootstrap']); } if (isset($this->arguments['printer']) && is_string($this->arguments['printer'])) { $this->arguments['printer'] = $this->handlePrinter($this->arguments['printer']); } if (isset($this->arguments['test']) && is_string($this->arguments['test']) && substr($this->arguments['test'], -5, 5) == '.phpt') { $test = new PHPUnit_Extensions_PhptTestCase($this->arguments['test']); $this->arguments['test'] = new PHPUnit_Framework_TestSuite; $this->arguments['test']->addTest($test); } if (!isset($this->arguments['test']) || (isset($this->arguments['testDatabaseLogRevision']) && !isset($this->arguments['testDatabaseDSN']))) { $this->showHelp(); exit(PHPUnit_TextUI_TestRunner::EXCEPTION_EXIT); } } /** * Handles the loading of the PHPUnit_Runner_TestSuiteLoader implementation. * * @param string $loaderClass * @param string $loaderFile * * @return PHPUnit_Runner_TestSuiteLoader */ protected function handleLoader($loaderClass, $loaderFile = '') { if (!class_exists($loaderClass, false)) { if ($loaderFile == '') { $loaderFile = PHPUnit_Util_Filesystem::classNameToFilename( $loaderClass ); } $loaderFile = stream_resolve_include_path($loaderFile); if ($loaderFile) { require $loaderFile; } } if (class_exists($loaderClass, false)) { $class = new ReflectionClass($loaderClass); if ($class->implementsInterface('PHPUnit_Runner_TestSuiteLoader') && $class->isInstantiable()) { return $class->newInstance(); } } if ($loaderClass == 'PHPUnit_Runner_StandardTestSuiteLoader') { return; } $this->showError( sprintf( 'Could not use "%s" as loader.', $loaderClass ) ); } /** * Handles the loading of the PHPUnit_Util_Printer implementation. * * @param string $printerClass * @param string $printerFile * * @return PHPUnit_Util_Printer */ protected function handlePrinter($printerClass, $printerFile = '') { if (!class_exists($printerClass, false)) { if ($printerFile == '') { $printerFile = PHPUnit_Util_Filesystem::classNameToFilename( $printerClass ); } $printerFile = stream_resolve_include_path($printerFile); if ($printerFile) { require $printerFile; } } if (class_exists($printerClass)) { $class = new ReflectionClass($printerClass); if ($class->implementsInterface('PHPUnit_Framework_TestListener') && $class->isSubclassOf('PHPUnit_Util_Printer') && $class->isInstantiable()) { if ($class->isSubclassOf('PHPUnit_TextUI_ResultPrinter')) { return $printerClass; } $outputStream = isset($this->arguments['stderr']) ? 'php://stderr' : null; return $class->newInstance($outputStream); } } $this->showError( sprintf( 'Could not use "%s" as printer.', $printerClass ) ); } /** * Loads a bootstrap file. * * @param string $filename */ protected function handleBootstrap($filename) { try { PHPUnit_Util_Fileloader::checkAndLoad($filename); } catch (PHPUnit_Framework_Exception $e) { $this->showError($e->getMessage()); } } /** * @since Method available since Release 4.0.0 */ protected function handleSelfUpdate($upgrade = false) { $this->printVersionString(); $localFilename = realpath($_SERVER['argv'][0]); if (!is_writable($localFilename)) { print 'No write permission to update ' . $localFilename . "\n"; exit(PHPUnit_TextUI_TestRunner::EXCEPTION_EXIT); } if (!extension_loaded('openssl')) { print "The OpenSSL extension is not loaded.\n"; exit(PHPUnit_TextUI_TestRunner::EXCEPTION_EXIT); } if (!$upgrade) { $remoteFilename = sprintf( 'https://phar.phpunit.de/phpunit-%s.phar', file_get_contents( sprintf( 'https://phar.phpunit.de/latest-version-of/phpunit-%s', PHPUnit_Runner_Version::series() ) ) ); } else { $remoteFilename = sprintf( 'https://phar.phpunit.de/phpunit%s.phar', PHPUnit_Runner_Version::getReleaseChannel() ); } $tempFilename = tempnam(sys_get_temp_dir(), 'phpunit') . '.phar'; // Workaround for https://bugs.php.net/bug.php?id=65538 $caFile = dirname($tempFilename) . '/ca.pem'; copy(__PHPUNIT_PHAR_ROOT__ . '/ca.pem', $caFile); print 'Updating the PHPUnit PHAR ... '; $options = array( 'ssl' => array( 'allow_self_signed' => false, 'cafile' => $caFile, 'verify_peer' => true ) ); if (PHP_VERSION_ID < 50600) { $options['ssl']['CN_match'] = 'phar.phpunit.de'; $options['ssl']['SNI_server_name'] = 'phar.phpunit.de'; } file_put_contents( $tempFilename, file_get_contents( $remoteFilename, false, stream_context_create($options) ) ); chmod($tempFilename, 0777 & ~umask()); try { $phar = new Phar($tempFilename); unset($phar); rename($tempFilename, $localFilename); unlink($caFile); } catch (Throwable $_e) { $e = $_e; } catch (Exception $_e) { $e = $_e; } if (isset($e)) { unlink($caFile); unlink($tempFilename); print " done\n\n" . $e->getMessage() . "\n"; exit(2); } print " done\n"; exit(PHPUnit_TextUI_TestRunner::SUCCESS_EXIT); } /** * @since Method available since Release 4.8.0 */ protected function handleVersionCheck() { $this->printVersionString(); $latestVersion = file_get_contents('https://phar.phpunit.de/latest-version-of/phpunit'); $isOutdated = version_compare($latestVersion, PHPUnit_Runner_Version::id(), '>'); if ($isOutdated) { print "You are not using the latest version of PHPUnit.\n"; print 'Use "phpunit --self-upgrade" to install PHPUnit ' . $latestVersion . "\n"; } else { print "You are using the latest version of PHPUnit.\n"; } exit(PHPUnit_TextUI_TestRunner::SUCCESS_EXIT); } /** * Show the help message. */ protected function showHelp() { $this->printVersionString(); print << Code Coverage Options: --coverage-clover Generate code coverage report in Clover XML format. --coverage-crap4j Generate code coverage report in Crap4J XML format. --coverage-html Generate code coverage report in HTML format. --coverage-php Export PHP_CodeCoverage object to file. --coverage-text= Generate code coverage report in text format. Default: Standard output. --coverage-xml Generate code coverage report in PHPUnit XML format. Logging Options: --log-junit Log test execution in JUnit XML format to file. --log-tap Log test execution in TAP format to file. --log-json Log test execution in JSON format. --testdox-html Write agile documentation in HTML format to file. --testdox-text Write agile documentation in Text format to file. Test Selection Options: --filter Filter which tests to run. --testsuite Filter which testsuite to run. --group ... Only runs tests from the specified group(s). --exclude-group ... Exclude tests from the specified group(s). --list-groups List available test groups. --test-suffix ... Only search for test in files with specified suffix(es). Default: Test.php,.phpt Test Execution Options: --report-useless-tests Be strict about tests that do not test anything. --strict-coverage Be strict about unintentionally covered code. --strict-global-state Be strict about changes to global state --disallow-test-output Be strict about output during tests. --enforce-time-limit Enforce time limit based on test size. --disallow-todo-tests Disallow @todo-annotated tests. --process-isolation Run each test in a separate PHP process. --no-globals-backup Do not backup and restore \$GLOBALS for each test. --static-backup Backup and restore static attributes for each test. --colors= Use colors in output ("never", "auto" or "always"). --columns Number of columns to use for progress output. --columns max Use maximum number of columns for progress output. --stderr Write to STDERR instead of STDOUT. --stop-on-error Stop execution upon first error. --stop-on-failure Stop execution upon first error or failure. --stop-on-risky Stop execution upon first risky test. --stop-on-skipped Stop execution upon first skipped test. --stop-on-incomplete Stop execution upon first incomplete test. -v|--verbose Output more verbose information. --debug Display debugging information during test execution. --loader TestSuiteLoader implementation to use. --repeat Runs the test(s) repeatedly. --tap Report test execution progress in TAP format. --testdox Report test execution progress in TestDox format. --printer TestListener implementation to use. Configuration Options: --bootstrap A "bootstrap" PHP file that is run before the tests. -c|--configuration Read configuration from XML file. --no-configuration Ignore default configuration file (phpunit.xml). --no-coverage Ignore code coverage configuration. --include-path Prepend PHP's include_path with given path(s). -d key[=value] Sets a php.ini value. Miscellaneous Options: -h|--help Prints this usage information. --version Prints the version and exits. EOT; if (defined('__PHPUNIT_PHAR__')) { print "\n --check-version Check whether PHPUnit is the latest version."; print "\n --self-update Update PHPUnit to the latest version within the same\n release series.\n"; print "\n --self-upgrade Upgrade PHPUnit to the latest version.\n"; } } /** * Custom callback for test suite discovery. */ protected function handleCustomTestSuite() { } private function printVersionString() { if ($this->versionStringPrinted) { return; } print PHPUnit_Runner_Version::getVersionString() . "\n\n"; $this->versionStringPrinted = true; } /** */ private function showError($message) { $this->printVersionString(); print $message . "\n"; exit(PHPUnit_TextUI_TestRunner::FAILURE_EXIT); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Environment\Console; /** * Prints the result of a TextUI TestRunner run. * * @since Class available since Release 2.0.0 */ class PHPUnit_TextUI_ResultPrinter extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener { const EVENT_TEST_START = 0; const EVENT_TEST_END = 1; const EVENT_TESTSUITE_START = 2; const EVENT_TESTSUITE_END = 3; const COLOR_NEVER = 'never'; const COLOR_AUTO = 'auto'; const COLOR_ALWAYS = 'always'; const COLOR_DEFAULT = self::COLOR_NEVER; /** * @var array */ private static $ansiCodes = array( 'bold' => 1, 'fg-black' => 30, 'fg-red' => 31, 'fg-green' => 32, 'fg-yellow' => 33, 'fg-blue' => 34, 'fg-magenta' => 35, 'fg-cyan' => 36, 'fg-white' => 37, 'bg-black' => 40, 'bg-red' => 41, 'bg-green' => 42, 'bg-yellow' => 43, 'bg-blue' => 44, 'bg-magenta' => 45, 'bg-cyan' => 46, 'bg-white' => 47 ); /** * @var int */ protected $column = 0; /** * @var int */ protected $maxColumn; /** * @var bool */ protected $lastTestFailed = false; /** * @var int */ protected $numAssertions = 0; /** * @var int */ protected $numTests = -1; /** * @var int */ protected $numTestsRun = 0; /** * @var int */ protected $numTestsWidth; /** * @var bool */ protected $colors = false; /** * @var bool */ protected $debug = false; /** * @var bool */ protected $verbose = false; /** * @var int */ private $numberOfColumns; /** * Constructor. * * @param mixed $out * @param bool $verbose * @param string $colors * @param bool $debug * @param int|string $numberOfColumns * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.0.0 */ public function __construct($out = null, $verbose = false, $colors = self::COLOR_DEFAULT, $debug = false, $numberOfColumns = 80) { parent::__construct($out); if (!is_bool($verbose)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'boolean'); } $availableColors = array(self::COLOR_NEVER, self::COLOR_AUTO, self::COLOR_ALWAYS); if (!in_array($colors, $availableColors)) { throw PHPUnit_Util_InvalidArgumentHelper::factory( 3, vsprintf('value from "%s", "%s" or "%s"', $availableColors) ); } if (!is_bool($debug)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(4, 'boolean'); } if (!is_int($numberOfColumns) && $numberOfColumns != 'max') { throw PHPUnit_Util_InvalidArgumentHelper::factory(5, 'integer or "max"'); } $console = new Console; $maxNumberOfColumns = $console->getNumberOfColumns(); if ($numberOfColumns == 'max' || $numberOfColumns > $maxNumberOfColumns) { $numberOfColumns = $maxNumberOfColumns; } $this->numberOfColumns = $numberOfColumns; $this->verbose = $verbose; $this->debug = $debug; if ($colors === self::COLOR_AUTO && $console->hasColorSupport()) { $this->colors = true; } else { $this->colors = (self::COLOR_ALWAYS === $colors); } } /** * @param PHPUnit_Framework_TestResult $result */ public function printResult(PHPUnit_Framework_TestResult $result) { $this->printHeader(); $this->printErrors($result); $printSeparator = $result->errorCount() > 0; if ($printSeparator && $result->failureCount() > 0) { $this->write("\n--\n\n"); } $printSeparator = $printSeparator || $result->failureCount() > 0; $this->printFailures($result); if ($this->verbose) { if ($printSeparator && $result->riskyCount() > 0) { $this->write("\n--\n\n"); } $printSeparator = $printSeparator || $result->riskyCount() > 0; $this->printRisky($result); if ($printSeparator && $result->notImplementedCount() > 0) { $this->write("\n--\n\n"); } $printSeparator = $printSeparator || $result->notImplementedCount() > 0; $this->printIncompletes($result); if ($printSeparator && $result->skippedCount() > 0) { $this->write("\n--\n\n"); } $this->printSkipped($result); } $this->printFooter($result); } /** * @param array $defects * @param string $type */ protected function printDefects(array $defects, $type) { $count = count($defects); if ($count == 0) { return; } $this->write( sprintf( "There %s %d %s%s:\n", ($count == 1) ? 'was' : 'were', $count, $type, ($count == 1) ? '' : 's' ) ); $i = 1; foreach ($defects as $defect) { $this->printDefect($defect, $i++); } } /** * @param PHPUnit_Framework_TestFailure $defect * @param int $count */ protected function printDefect(PHPUnit_Framework_TestFailure $defect, $count) { $this->printDefectHeader($defect, $count); $this->printDefectTrace($defect); } /** * @param PHPUnit_Framework_TestFailure $defect * @param int $count */ protected function printDefectHeader(PHPUnit_Framework_TestFailure $defect, $count) { $this->write( sprintf( "\n%d) %s\n", $count, $defect->getTestName() ) ); } /** * @param PHPUnit_Framework_TestFailure $defect */ protected function printDefectTrace(PHPUnit_Framework_TestFailure $defect) { $e = $defect->thrownException(); $this->write((string) $e); while ($e = $e->getPrevious()) { $this->write("\nCaused by\n" . $e); } } /** * @param PHPUnit_Framework_TestResult $result */ protected function printErrors(PHPUnit_Framework_TestResult $result) { $this->printDefects($result->errors(), 'error'); } /** * @param PHPUnit_Framework_TestResult $result */ protected function printFailures(PHPUnit_Framework_TestResult $result) { $this->printDefects($result->failures(), 'failure'); } /** * @param PHPUnit_Framework_TestResult $result */ protected function printIncompletes(PHPUnit_Framework_TestResult $result) { $this->printDefects($result->notImplemented(), 'incomplete test'); } /** * @param PHPUnit_Framework_TestResult $result * * @since Method available since Release 4.0.0 */ protected function printRisky(PHPUnit_Framework_TestResult $result) { $this->printDefects($result->risky(), 'risky test'); } /** * @param PHPUnit_Framework_TestResult $result * * @since Method available since Release 3.0.0 */ protected function printSkipped(PHPUnit_Framework_TestResult $result) { $this->printDefects($result->skipped(), 'skipped test'); } protected function printHeader() { $this->write("\n\n" . PHP_Timer::resourceUsage() . "\n\n"); } /** * @param PHPUnit_Framework_TestResult $result */ protected function printFooter(PHPUnit_Framework_TestResult $result) { if (count($result) === 0) { $this->writeWithColor( 'fg-black, bg-yellow', 'No tests executed!' ); } elseif ($result->wasSuccessful() && $result->allHarmless() && $result->allCompletelyImplemented() && $result->noneSkipped()) { $this->writeWithColor( 'fg-black, bg-green', sprintf( 'OK (%d test%s, %d assertion%s)', count($result), (count($result) == 1) ? '' : 's', $this->numAssertions, ($this->numAssertions == 1) ? '' : 's' ) ); } else { if ($result->wasSuccessful()) { $color = 'fg-black, bg-yellow'; if ($this->verbose) { $this->write("\n"); } $this->writeWithColor( $color, 'OK, but incomplete, skipped, or risky tests!' ); } else { $color = 'fg-white, bg-red'; $this->write("\n"); $this->writeWithColor($color, 'FAILURES!'); } $this->writeCountString(count($result), 'Tests', $color, true); $this->writeCountString($this->numAssertions, 'Assertions', $color, true); $this->writeCountString($result->errorCount(), 'Errors', $color); $this->writeCountString($result->failureCount(), 'Failures', $color); $this->writeCountString($result->skippedCount(), 'Skipped', $color); $this->writeCountString($result->notImplementedCount(), 'Incomplete', $color); $this->writeCountString($result->riskyCount(), 'Risky', $color); $this->writeWithColor($color, '.', true); } } /** */ public function printWaitPrompt() { $this->write("\n to continue\n"); } /** * An error occurred. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->writeProgressWithColor('fg-red, bold', 'E'); $this->lastTestFailed = true; } /** * A failure occurred. * * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_AssertionFailedError $e * @param float $time */ public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { $this->writeProgressWithColor('bg-red, fg-white', 'F'); $this->lastTestFailed = true; } /** * Incomplete test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->writeProgressWithColor('fg-yellow, bold', 'I'); $this->lastTestFailed = true; } /** * Risky test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 4.0.0 */ public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->writeProgressWithColor('fg-yellow, bold', 'R'); $this->lastTestFailed = true; } /** * Skipped test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 3.0.0 */ public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->writeProgressWithColor('fg-cyan, bold', 'S'); $this->lastTestFailed = true; } /** * A testsuite started. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function startTestSuite(PHPUnit_Framework_TestSuite $suite) { if ($this->numTests == -1) { $this->numTests = count($suite); $this->numTestsWidth = strlen((string) $this->numTests); $this->maxColumn = $this->numberOfColumns - strlen(' / (XXX%)') - (2 * $this->numTestsWidth); } } /** * A testsuite ended. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function endTestSuite(PHPUnit_Framework_TestSuite $suite) { } /** * A test started. * * @param PHPUnit_Framework_Test $test */ public function startTest(PHPUnit_Framework_Test $test) { if ($this->debug) { $this->write( sprintf( "\nStarting test '%s'.\n", PHPUnit_Util_Test::describe($test) ) ); } } /** * A test ended. * * @param PHPUnit_Framework_Test $test * @param float $time */ public function endTest(PHPUnit_Framework_Test $test, $time) { if (!$this->lastTestFailed) { $this->writeProgress('.'); } if ($test instanceof PHPUnit_Framework_TestCase) { $this->numAssertions += $test->getNumAssertions(); } elseif ($test instanceof PHPUnit_Extensions_PhptTestCase) { $this->numAssertions++; } $this->lastTestFailed = false; if ($test instanceof PHPUnit_Framework_TestCase) { if (!$test->hasExpectationOnOutput()) { $this->write($test->getActualOutput()); } } } /** * @param string $progress */ protected function writeProgress($progress) { $this->write($progress); $this->column++; $this->numTestsRun++; if ($this->column == $this->maxColumn) { $this->write( sprintf( ' %' . $this->numTestsWidth . 'd / %' . $this->numTestsWidth . 'd (%3s%%)', $this->numTestsRun, $this->numTests, floor(($this->numTestsRun / $this->numTests) * 100) ) ); $this->writeNewLine(); } } protected function writeNewLine() { $this->column = 0; $this->write("\n"); } /** * Formats a buffer with a specified ANSI color sequence if colors are * enabled. * * @param string $color * @param string $buffer * * @return string * * @since Method available since Release 4.0.0 */ protected function formatWithColor($color, $buffer) { if (!$this->colors) { return $buffer; } $codes = array_map('trim', explode(',', $color)); $lines = explode("\n", $buffer); $padding = max(array_map('strlen', $lines)); $styles = array(); foreach ($codes as $code) { $styles[] = self::$ansiCodes[$code]; } $style = sprintf("\x1b[%sm", implode(';', $styles)); $styledLines = array(); foreach ($lines as $line) { $styledLines[] = $style . str_pad($line, $padding) . "\x1b[0m"; } return implode("\n", $styledLines); } /** * Writes a buffer out with a color sequence if colors are enabled. * * @param string $color * @param string $buffer * @param bool $lf * * @since Method available since Release 4.0.0 */ protected function writeWithColor($color, $buffer, $lf = true) { $this->write($this->formatWithColor($color, $buffer)); if ($lf) { $this->write("\n"); } } /** * Writes progress with a color sequence if colors are enabled. * * @param string $color * @param string $buffer * * @since Method available since Release 4.0.0 */ protected function writeProgressWithColor($color, $buffer) { $buffer = $this->formatWithColor($color, $buffer); $this->writeProgress($buffer); } /** * @param int $count * @param string $name * @param string $color * @param bool $always * * @since Method available since Release 4.6.5 */ private function writeCountString($count, $name, $color, $always = false) { static $first = true; if ($always || $count > 0) { $this->writeWithColor( $color, sprintf( '%s%s: %d', !$first ? ', ' : '', $name, $count ), false ); $first = false; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Environment\Runtime; /** * A TestRunner for the Command Line Interface (CLI) * PHP SAPI Module. * * @since Class available since Release 2.0.0 */ class PHPUnit_TextUI_TestRunner extends PHPUnit_Runner_BaseTestRunner { const SUCCESS_EXIT = 0; const FAILURE_EXIT = 1; const EXCEPTION_EXIT = 2; /** * @var PHP_CodeCoverage_Filter */ protected $codeCoverageFilter; /** * @var PHPUnit_Runner_TestSuiteLoader */ protected $loader = null; /** * @var PHPUnit_TextUI_ResultPrinter */ protected $printer = null; /** * @var bool */ protected static $versionStringPrinted = false; /** * @var array */ private $missingExtensions = array(); /** * @var Runtime */ private $runtime; /** * @param PHPUnit_Runner_TestSuiteLoader $loader * @param PHP_CodeCoverage_Filter $filter * * @since Method available since Release 3.4.0 */ public function __construct(PHPUnit_Runner_TestSuiteLoader $loader = null, PHP_CodeCoverage_Filter $filter = null) { if ($filter === null) { $filter = $this->getCodeCoverageFilter(); } $this->codeCoverageFilter = $filter; $this->loader = $loader; $this->runtime = new Runtime; } /** * @param PHPUnit_Framework_Test|ReflectionClass $test * @param array $arguments * * @return PHPUnit_Framework_TestResult * * @throws PHPUnit_Framework_Exception */ public static function run($test, array $arguments = array()) { if ($test instanceof ReflectionClass) { $test = new PHPUnit_Framework_TestSuite($test); } if ($test instanceof PHPUnit_Framework_Test) { $aTestRunner = new self; return $aTestRunner->doRun( $test, $arguments ); } else { throw new PHPUnit_Framework_Exception( 'No test case or test suite found.' ); } } /** * @return PHPUnit_Framework_TestResult */ protected function createTestResult() { return new PHPUnit_Framework_TestResult; } private function processSuiteFilters(PHPUnit_Framework_TestSuite $suite, array $arguments) { if (!$arguments['filter'] && empty($arguments['groups']) && empty($arguments['excludeGroups'])) { return; } $filterFactory = new PHPUnit_Runner_Filter_Factory(); if (!empty($arguments['excludeGroups'])) { $filterFactory->addFilter( new ReflectionClass('PHPUnit_Runner_Filter_Group_Exclude'), $arguments['excludeGroups'] ); } if (!empty($arguments['groups'])) { $filterFactory->addFilter( new ReflectionClass('PHPUnit_Runner_Filter_Group_Include'), $arguments['groups'] ); } if ($arguments['filter']) { $filterFactory->addFilter( new ReflectionClass('PHPUnit_Runner_Filter_Test'), $arguments['filter'] ); } $suite->injectFilter($filterFactory); } /** * @param PHPUnit_Framework_Test $suite * @param array $arguments * * @return PHPUnit_Framework_TestResult */ public function doRun(PHPUnit_Framework_Test $suite, array $arguments = array()) { if (isset($arguments['configuration'])) { $GLOBALS['__PHPUNIT_CONFIGURATION_FILE'] = $arguments['configuration']; } $this->handleConfiguration($arguments); $this->processSuiteFilters($suite, $arguments); if (isset($arguments['bootstrap'])) { $GLOBALS['__PHPUNIT_BOOTSTRAP'] = $arguments['bootstrap']; } if ($arguments['backupGlobals'] === false) { $suite->setBackupGlobals(false); } if ($arguments['backupStaticAttributes'] === true) { $suite->setBackupStaticAttributes(true); } if ($arguments['disallowChangesToGlobalState'] === true) { $suite->setDisallowChangesToGlobalState(true); } if (is_integer($arguments['repeat'])) { $test = new PHPUnit_Extensions_RepeatedTest( $suite, $arguments['repeat'], $arguments['processIsolation'] ); $suite = new PHPUnit_Framework_TestSuite(); $suite->addTest($test); } $result = $this->createTestResult(); if (!$arguments['convertErrorsToExceptions']) { $result->convertErrorsToExceptions(false); } if (!$arguments['convertNoticesToExceptions']) { PHPUnit_Framework_Error_Notice::$enabled = false; } if (!$arguments['convertWarningsToExceptions']) { PHPUnit_Framework_Error_Warning::$enabled = false; } if ($arguments['stopOnError']) { $result->stopOnError(true); } if ($arguments['stopOnFailure']) { $result->stopOnFailure(true); } if ($arguments['stopOnIncomplete']) { $result->stopOnIncomplete(true); } if ($arguments['stopOnRisky']) { $result->stopOnRisky(true); } if ($arguments['stopOnSkipped']) { $result->stopOnSkipped(true); } if ($this->printer === null) { if (isset($arguments['printer']) && $arguments['printer'] instanceof PHPUnit_Util_Printer) { $this->printer = $arguments['printer']; } else { $printerClass = 'PHPUnit_TextUI_ResultPrinter'; if (isset($arguments['printer']) && is_string($arguments['printer']) && class_exists($arguments['printer'], false)) { $class = new ReflectionClass($arguments['printer']); if ($class->isSubclassOf('PHPUnit_TextUI_ResultPrinter')) { $printerClass = $arguments['printer']; } } $this->printer = new $printerClass( isset($arguments['stderr']) ? 'php://stderr' : null, $arguments['verbose'], $arguments['colors'], $arguments['debug'], $arguments['columns'] ); } } if (!$this->printer instanceof PHPUnit_Util_Log_TAP) { $this->printer->write( PHPUnit_Runner_Version::getVersionString() . "\n" ); self::$versionStringPrinted = true; if ($arguments['verbose']) { $this->printer->write( sprintf( "\nRuntime:\t%s", $this->runtime->getNameWithVersion() ) ); if ($this->runtime->hasXdebug()) { $this->printer->write( sprintf( ' with Xdebug %s', phpversion('xdebug') ) ); } if (isset($arguments['configuration'])) { $this->printer->write( sprintf( "\nConfiguration:\t%s", $arguments['configuration']->getFilename() ) ); } $this->printer->write("\n"); } if (isset($arguments['deprecatedStrictModeOption'])) { print "Warning:\tDeprecated option \"--strict\" used\n"; } elseif (isset($arguments['deprecatedStrictModeSetting'])) { print "Warning:\tDeprecated configuration setting \"strict\" used\n"; } if (isset($arguments['deprecatedSeleniumConfiguration'])) { print "Warning:\tDeprecated configuration setting \"selenium\" used\n"; } } foreach ($arguments['listeners'] as $listener) { $result->addListener($listener); } $result->addListener($this->printer); if (isset($arguments['testdoxHTMLFile'])) { $result->addListener( new PHPUnit_Util_TestDox_ResultPrinter_HTML( $arguments['testdoxHTMLFile'] ) ); } if (isset($arguments['testdoxTextFile'])) { $result->addListener( new PHPUnit_Util_TestDox_ResultPrinter_Text( $arguments['testdoxTextFile'] ) ); } $codeCoverageReports = 0; if (isset($arguments['coverageClover'])) { $codeCoverageReports++; } if (isset($arguments['coverageCrap4J'])) { $codeCoverageReports++; } if (isset($arguments['coverageHtml'])) { $codeCoverageReports++; } if (isset($arguments['coveragePHP'])) { $codeCoverageReports++; } if (isset($arguments['coverageText'])) { $codeCoverageReports++; } if (isset($arguments['coverageXml'])) { $codeCoverageReports++; } if (isset($arguments['noCoverage'])) { $codeCoverageReports = 0; } if ($codeCoverageReports > 0 && (!extension_loaded('tokenizer') || !$this->runtime->canCollectCodeCoverage())) { if (!extension_loaded('tokenizer')) { $this->showExtensionNotLoadedWarning( 'tokenizer', 'No code coverage will be generated.' ); } elseif (!extension_loaded('Xdebug')) { $this->showExtensionNotLoadedWarning( 'Xdebug', 'No code coverage will be generated.' ); } $codeCoverageReports = 0; } if (!$this->printer instanceof PHPUnit_Util_Log_TAP) { if ($codeCoverageReports > 0 && !$this->codeCoverageFilter->hasWhitelist()) { $this->printer->write("Warning:\tNo whitelist configured for code coverage\n"); } $this->printer->write("\n"); } if ($codeCoverageReports > 0) { $codeCoverage = new PHP_CodeCoverage( null, $this->codeCoverageFilter ); $codeCoverage->setAddUncoveredFilesFromWhitelist( $arguments['addUncoveredFilesFromWhitelist'] ); $codeCoverage->setCheckForUnintentionallyCoveredCode( $arguments['strictCoverage'] ); $codeCoverage->setProcessUncoveredFilesFromWhitelist( $arguments['processUncoveredFilesFromWhitelist'] ); if (isset($arguments['forceCoversAnnotation'])) { $codeCoverage->setForceCoversAnnotation( $arguments['forceCoversAnnotation'] ); } if (isset($arguments['mapTestClassNameToCoveredClassName'])) { $codeCoverage->setMapTestClassNameToCoveredClassName( $arguments['mapTestClassNameToCoveredClassName'] ); } $result->setCodeCoverage($codeCoverage); } if ($codeCoverageReports > 1) { if (isset($arguments['cacheTokens'])) { $codeCoverage->setCacheTokens($arguments['cacheTokens']); } } if (isset($arguments['jsonLogfile'])) { $result->addListener( new PHPUnit_Util_Log_JSON($arguments['jsonLogfile']) ); } if (isset($arguments['tapLogfile'])) { $result->addListener( new PHPUnit_Util_Log_TAP($arguments['tapLogfile']) ); } if (isset($arguments['junitLogfile'])) { $result->addListener( new PHPUnit_Util_Log_JUnit( $arguments['junitLogfile'], $arguments['logIncompleteSkipped'] ) ); } $result->beStrictAboutTestsThatDoNotTestAnything($arguments['reportUselessTests']); $result->beStrictAboutOutputDuringTests($arguments['disallowTestOutput']); $result->beStrictAboutTodoAnnotatedTests($arguments['disallowTodoAnnotatedTests']); $result->beStrictAboutTestSize($arguments['enforceTimeLimit']); $result->setTimeoutForSmallTests($arguments['timeoutForSmallTests']); $result->setTimeoutForMediumTests($arguments['timeoutForMediumTests']); $result->setTimeoutForLargeTests($arguments['timeoutForLargeTests']); if ($suite instanceof PHPUnit_Framework_TestSuite) { $suite->setRunTestInSeparateProcess($arguments['processIsolation']); } $suite->run($result); unset($suite); $result->flushListeners(); if ($this->printer instanceof PHPUnit_TextUI_ResultPrinter) { $this->printer->printResult($result); } if (isset($codeCoverage)) { if (isset($arguments['coverageClover'])) { $this->printer->write( "\nGenerating code coverage report in Clover XML format ..." ); try { $writer = new PHP_CodeCoverage_Report_Clover; $writer->process($codeCoverage, $arguments['coverageClover']); $this->printer->write(" done\n"); unset($writer); } catch (PHP_CodeCoverage_Exception $e) { $this->printer->write( " failed\n" . $e->getMessage() . "\n" ); } } if (isset($arguments['coverageCrap4J'])) { $this->printer->write( "\nGenerating Crap4J report XML file ..." ); try { $writer = new PHP_CodeCoverage_Report_Crap4j($arguments['crap4jThreshold']); $writer->process($codeCoverage, $arguments['coverageCrap4J']); $this->printer->write(" done\n"); unset($writer); } catch (PHP_CodeCoverage_Exception $e) { $this->printer->write( " failed\n" . $e->getMessage() . "\n" ); } } if (isset($arguments['coverageHtml'])) { $this->printer->write( "\nGenerating code coverage report in HTML format ..." ); try { $writer = new PHP_CodeCoverage_Report_HTML( $arguments['reportLowUpperBound'], $arguments['reportHighLowerBound'], sprintf( ' and PHPUnit %s', PHPUnit_Runner_Version::id() ) ); $writer->process($codeCoverage, $arguments['coverageHtml']); $this->printer->write(" done\n"); unset($writer); } catch (PHP_CodeCoverage_Exception $e) { $this->printer->write( " failed\n" . $e->getMessage() . "\n" ); } } if (isset($arguments['coveragePHP'])) { $this->printer->write( "\nGenerating code coverage report in PHP format ..." ); try { $writer = new PHP_CodeCoverage_Report_PHP; $writer->process($codeCoverage, $arguments['coveragePHP']); $this->printer->write(" done\n"); unset($writer); } catch (PHP_CodeCoverage_Exception $e) { $this->printer->write( " failed\n" . $e->getMessage() . "\n" ); } } if (isset($arguments['coverageText'])) { if ($arguments['coverageText'] == 'php://stdout') { $outputStream = $this->printer; $colors = $arguments['colors'] && $arguments['colors'] != PHPUnit_TextUI_ResultPrinter::COLOR_NEVER; } else { $outputStream = new PHPUnit_Util_Printer($arguments['coverageText']); $colors = false; } $processor = new PHP_CodeCoverage_Report_Text( $arguments['reportLowUpperBound'], $arguments['reportHighLowerBound'], $arguments['coverageTextShowUncoveredFiles'], $arguments['coverageTextShowOnlySummary'] ); $outputStream->write( $processor->process($codeCoverage, $colors) ); } if (isset($arguments['coverageXml'])) { $this->printer->write( "\nGenerating code coverage report in PHPUnit XML format ..." ); try { $writer = new PHP_CodeCoverage_Report_XML; $writer->process($codeCoverage, $arguments['coverageXml']); $this->printer->write(" done\n"); unset($writer); } catch (PHP_CodeCoverage_Exception $e) { $this->printer->write( " failed\n" . $e->getMessage() . "\n" ); } } } return $result; } /** * @param PHPUnit_TextUI_ResultPrinter $resultPrinter */ public function setPrinter(PHPUnit_TextUI_ResultPrinter $resultPrinter) { $this->printer = $resultPrinter; } /** * Override to define how to handle a failed loading of * a test suite. * * @param string $message */ protected function runFailed($message) { $this->write($message . PHP_EOL); exit(self::FAILURE_EXIT); } /** * @param string $buffer * * @since Method available since Release 3.1.0 */ protected function write($buffer) { if (PHP_SAPI != 'cli' && PHP_SAPI != 'phpdbg') { $buffer = htmlspecialchars($buffer); } if ($this->printer !== null) { $this->printer->write($buffer); } else { print $buffer; } } /** * Returns the loader to be used. * * @return PHPUnit_Runner_TestSuiteLoader * * @since Method available since Release 2.2.0 */ public function getLoader() { if ($this->loader === null) { $this->loader = new PHPUnit_Runner_StandardTestSuiteLoader; } return $this->loader; } /** * @param array $arguments * * @since Method available since Release 3.2.1 */ protected function handleConfiguration(array &$arguments) { if (isset($arguments['configuration']) && !$arguments['configuration'] instanceof PHPUnit_Util_Configuration) { $arguments['configuration'] = PHPUnit_Util_Configuration::getInstance( $arguments['configuration'] ); } $arguments['debug'] = isset($arguments['debug']) ? $arguments['debug'] : false; $arguments['filter'] = isset($arguments['filter']) ? $arguments['filter'] : false; $arguments['listeners'] = isset($arguments['listeners']) ? $arguments['listeners'] : array(); if (isset($arguments['configuration'])) { $arguments['configuration']->handlePHPConfiguration(); $phpunitConfiguration = $arguments['configuration']->getPHPUnitConfiguration(); if (isset($phpunitConfiguration['deprecatedStrictModeSetting'])) { $arguments['deprecatedStrictModeSetting'] = true; } if (isset($phpunitConfiguration['backupGlobals']) && !isset($arguments['backupGlobals'])) { $arguments['backupGlobals'] = $phpunitConfiguration['backupGlobals']; } if (isset($phpunitConfiguration['backupStaticAttributes']) && !isset($arguments['backupStaticAttributes'])) { $arguments['backupStaticAttributes'] = $phpunitConfiguration['backupStaticAttributes']; } if (isset($phpunitConfiguration['disallowChangesToGlobalState']) && !isset($arguments['disallowChangesToGlobalState'])) { $arguments['disallowChangesToGlobalState'] = $phpunitConfiguration['disallowChangesToGlobalState']; } if (isset($phpunitConfiguration['bootstrap']) && !isset($arguments['bootstrap'])) { $arguments['bootstrap'] = $phpunitConfiguration['bootstrap']; } if (isset($phpunitConfiguration['cacheTokens']) && !isset($arguments['cacheTokens'])) { $arguments['cacheTokens'] = $phpunitConfiguration['cacheTokens']; } if (isset($phpunitConfiguration['colors']) && !isset($arguments['colors'])) { $arguments['colors'] = $phpunitConfiguration['colors']; } if (isset($phpunitConfiguration['convertErrorsToExceptions']) && !isset($arguments['convertErrorsToExceptions'])) { $arguments['convertErrorsToExceptions'] = $phpunitConfiguration['convertErrorsToExceptions']; } if (isset($phpunitConfiguration['convertNoticesToExceptions']) && !isset($arguments['convertNoticesToExceptions'])) { $arguments['convertNoticesToExceptions'] = $phpunitConfiguration['convertNoticesToExceptions']; } if (isset($phpunitConfiguration['convertWarningsToExceptions']) && !isset($arguments['convertWarningsToExceptions'])) { $arguments['convertWarningsToExceptions'] = $phpunitConfiguration['convertWarningsToExceptions']; } if (isset($phpunitConfiguration['processIsolation']) && !isset($arguments['processIsolation'])) { $arguments['processIsolation'] = $phpunitConfiguration['processIsolation']; } if (isset($phpunitConfiguration['stopOnError']) && !isset($arguments['stopOnError'])) { $arguments['stopOnError'] = $phpunitConfiguration['stopOnError']; } if (isset($phpunitConfiguration['stopOnFailure']) && !isset($arguments['stopOnFailure'])) { $arguments['stopOnFailure'] = $phpunitConfiguration['stopOnFailure']; } if (isset($phpunitConfiguration['stopOnIncomplete']) && !isset($arguments['stopOnIncomplete'])) { $arguments['stopOnIncomplete'] = $phpunitConfiguration['stopOnIncomplete']; } if (isset($phpunitConfiguration['stopOnRisky']) && !isset($arguments['stopOnRisky'])) { $arguments['stopOnRisky'] = $phpunitConfiguration['stopOnRisky']; } if (isset($phpunitConfiguration['stopOnSkipped']) && !isset($arguments['stopOnSkipped'])) { $arguments['stopOnSkipped'] = $phpunitConfiguration['stopOnSkipped']; } if (isset($phpunitConfiguration['timeoutForSmallTests']) && !isset($arguments['timeoutForSmallTests'])) { $arguments['timeoutForSmallTests'] = $phpunitConfiguration['timeoutForSmallTests']; } if (isset($phpunitConfiguration['timeoutForMediumTests']) && !isset($arguments['timeoutForMediumTests'])) { $arguments['timeoutForMediumTests'] = $phpunitConfiguration['timeoutForMediumTests']; } if (isset($phpunitConfiguration['timeoutForLargeTests']) && !isset($arguments['timeoutForLargeTests'])) { $arguments['timeoutForLargeTests'] = $phpunitConfiguration['timeoutForLargeTests']; } if (isset($phpunitConfiguration['reportUselessTests']) && !isset($arguments['reportUselessTests'])) { $arguments['reportUselessTests'] = $phpunitConfiguration['reportUselessTests']; } if (isset($phpunitConfiguration['strictCoverage']) && !isset($arguments['strictCoverage'])) { $arguments['strictCoverage'] = $phpunitConfiguration['strictCoverage']; } if (isset($phpunitConfiguration['disallowTestOutput']) && !isset($arguments['disallowTestOutput'])) { $arguments['disallowTestOutput'] = $phpunitConfiguration['disallowTestOutput']; } if (isset($phpunitConfiguration['enforceTimeLimit']) && !isset($arguments['enforceTimeLimit'])) { $arguments['enforceTimeLimit'] = $phpunitConfiguration['enforceTimeLimit']; } if (isset($phpunitConfiguration['disallowTodoAnnotatedTests']) && !isset($arguments['disallowTodoAnnotatedTests'])) { $arguments['disallowTodoAnnotatedTests'] = $phpunitConfiguration['disallowTodoAnnotatedTests']; } if (isset($phpunitConfiguration['verbose']) && !isset($arguments['verbose'])) { $arguments['verbose'] = $phpunitConfiguration['verbose']; } if (isset($phpunitConfiguration['forceCoversAnnotation']) && !isset($arguments['forceCoversAnnotation'])) { $arguments['forceCoversAnnotation'] = $phpunitConfiguration['forceCoversAnnotation']; } if (isset($phpunitConfiguration['mapTestClassNameToCoveredClassName']) && !isset($arguments['mapTestClassNameToCoveredClassName'])) { $arguments['mapTestClassNameToCoveredClassName'] = $phpunitConfiguration['mapTestClassNameToCoveredClassName']; } $groupCliArgs = array(); if (!empty($arguments['groups'])) { $groupCliArgs = $arguments['groups']; } $groupConfiguration = $arguments['configuration']->getGroupConfiguration(); if (!empty($groupConfiguration['include']) && !isset($arguments['groups'])) { $arguments['groups'] = $groupConfiguration['include']; } if (!empty($groupConfiguration['exclude']) && !isset($arguments['excludeGroups'])) { $arguments['excludeGroups'] = array_diff($groupConfiguration['exclude'], $groupCliArgs); } foreach ($arguments['configuration']->getListenerConfiguration() as $listener) { if (!class_exists($listener['class'], false) && $listener['file'] !== '') { require_once $listener['file']; } if (class_exists($listener['class'])) { if (count($listener['arguments']) == 0) { $listener = new $listener['class']; } else { $listenerClass = new ReflectionClass( $listener['class'] ); $listener = $listenerClass->newInstanceArgs( $listener['arguments'] ); } if ($listener instanceof PHPUnit_Framework_TestListener) { $arguments['listeners'][] = $listener; } } } $loggingConfiguration = $arguments['configuration']->getLoggingConfiguration(); if (isset($loggingConfiguration['coverage-clover']) && !isset($arguments['coverageClover'])) { $arguments['coverageClover'] = $loggingConfiguration['coverage-clover']; } if (isset($loggingConfiguration['coverage-crap4j']) && !isset($arguments['coverageCrap4J'])) { $arguments['coverageCrap4J'] = $loggingConfiguration['coverage-crap4j']; if (isset($loggingConfiguration['crap4jThreshold']) && !isset($arguments['crap4jThreshold'])) { $arguments['crap4jThreshold'] = $loggingConfiguration['crap4jThreshold']; } } if (isset($loggingConfiguration['coverage-html']) && !isset($arguments['coverageHtml'])) { if (isset($loggingConfiguration['lowUpperBound']) && !isset($arguments['reportLowUpperBound'])) { $arguments['reportLowUpperBound'] = $loggingConfiguration['lowUpperBound']; } if (isset($loggingConfiguration['highLowerBound']) && !isset($arguments['reportHighLowerBound'])) { $arguments['reportHighLowerBound'] = $loggingConfiguration['highLowerBound']; } $arguments['coverageHtml'] = $loggingConfiguration['coverage-html']; } if (isset($loggingConfiguration['coverage-php']) && !isset($arguments['coveragePHP'])) { $arguments['coveragePHP'] = $loggingConfiguration['coverage-php']; } if (isset($loggingConfiguration['coverage-text']) && !isset($arguments['coverageText'])) { $arguments['coverageText'] = $loggingConfiguration['coverage-text']; if (isset($loggingConfiguration['coverageTextShowUncoveredFiles'])) { $arguments['coverageTextShowUncoveredFiles'] = $loggingConfiguration['coverageTextShowUncoveredFiles']; } else { $arguments['coverageTextShowUncoveredFiles'] = false; } if (isset($loggingConfiguration['coverageTextShowOnlySummary'])) { $arguments['coverageTextShowOnlySummary'] = $loggingConfiguration['coverageTextShowOnlySummary']; } else { $arguments['coverageTextShowOnlySummary'] = false; } } if (isset($loggingConfiguration['coverage-xml']) && !isset($arguments['coverageXml'])) { $arguments['coverageXml'] = $loggingConfiguration['coverage-xml']; } if (isset($loggingConfiguration['json']) && !isset($arguments['jsonLogfile'])) { $arguments['jsonLogfile'] = $loggingConfiguration['json']; } if (isset($loggingConfiguration['plain'])) { $arguments['listeners'][] = new PHPUnit_TextUI_ResultPrinter( $loggingConfiguration['plain'], true ); } if (isset($loggingConfiguration['tap']) && !isset($arguments['tapLogfile'])) { $arguments['tapLogfile'] = $loggingConfiguration['tap']; } if (isset($loggingConfiguration['junit']) && !isset($arguments['junitLogfile'])) { $arguments['junitLogfile'] = $loggingConfiguration['junit']; if (isset($loggingConfiguration['logIncompleteSkipped']) && !isset($arguments['logIncompleteSkipped'])) { $arguments['logIncompleteSkipped'] = $loggingConfiguration['logIncompleteSkipped']; } } if (isset($loggingConfiguration['testdox-html']) && !isset($arguments['testdoxHTMLFile'])) { $arguments['testdoxHTMLFile'] = $loggingConfiguration['testdox-html']; } if (isset($loggingConfiguration['testdox-text']) && !isset($arguments['testdoxTextFile'])) { $arguments['testdoxTextFile'] = $loggingConfiguration['testdox-text']; } if ((isset($arguments['coverageClover']) || isset($arguments['coverageCrap4J']) || isset($arguments['coverageHtml']) || isset($arguments['coveragePHP']) || isset($arguments['coverageText']) || isset($arguments['coverageXml'])) && $this->runtime->canCollectCodeCoverage()) { $filterConfiguration = $arguments['configuration']->getFilterConfiguration(); $arguments['addUncoveredFilesFromWhitelist'] = $filterConfiguration['whitelist']['addUncoveredFilesFromWhitelist']; $arguments['processUncoveredFilesFromWhitelist'] = $filterConfiguration['whitelist']['processUncoveredFilesFromWhitelist']; if (empty($filterConfiguration['whitelist']['include']['directory']) && empty($filterConfiguration['whitelist']['include']['file'])) { foreach ($filterConfiguration['blacklist']['include']['directory'] as $dir) { $this->codeCoverageFilter->addDirectoryToBlacklist( $dir['path'], $dir['suffix'], $dir['prefix'], $dir['group'] ); } foreach ($filterConfiguration['blacklist']['include']['file'] as $file) { $this->codeCoverageFilter->addFileToBlacklist($file); } foreach ($filterConfiguration['blacklist']['exclude']['directory'] as $dir) { $this->codeCoverageFilter->removeDirectoryFromBlacklist( $dir['path'], $dir['suffix'], $dir['prefix'], $dir['group'] ); } foreach ($filterConfiguration['blacklist']['exclude']['file'] as $file) { $this->codeCoverageFilter->removeFileFromBlacklist($file); } } foreach ($filterConfiguration['whitelist']['include']['directory'] as $dir) { $this->codeCoverageFilter->addDirectoryToWhitelist( $dir['path'], $dir['suffix'], $dir['prefix'] ); } foreach ($filterConfiguration['whitelist']['include']['file'] as $file) { $this->codeCoverageFilter->addFileToWhitelist($file); } foreach ($filterConfiguration['whitelist']['exclude']['directory'] as $dir) { $this->codeCoverageFilter->removeDirectoryFromWhitelist( $dir['path'], $dir['suffix'], $dir['prefix'] ); } foreach ($filterConfiguration['whitelist']['exclude']['file'] as $file) { $this->codeCoverageFilter->removeFileFromWhitelist($file); } } } $arguments['addUncoveredFilesFromWhitelist'] = isset($arguments['addUncoveredFilesFromWhitelist']) ? $arguments['addUncoveredFilesFromWhitelist'] : true; $arguments['processUncoveredFilesFromWhitelist'] = isset($arguments['processUncoveredFilesFromWhitelist']) ? $arguments['processUncoveredFilesFromWhitelist'] : false; $arguments['backupGlobals'] = isset($arguments['backupGlobals']) ? $arguments['backupGlobals'] : null; $arguments['backupStaticAttributes'] = isset($arguments['backupStaticAttributes']) ? $arguments['backupStaticAttributes'] : null; $arguments['disallowChangesToGlobalState'] = isset($arguments['disallowChangesToGlobalState']) ? $arguments['disallowChangesToGlobalState'] : null; $arguments['cacheTokens'] = isset($arguments['cacheTokens']) ? $arguments['cacheTokens'] : false; $arguments['columns'] = isset($arguments['columns']) ? $arguments['columns'] : 80; $arguments['colors'] = isset($arguments['colors']) ? $arguments['colors'] : PHPUnit_TextUI_ResultPrinter::COLOR_DEFAULT; $arguments['convertErrorsToExceptions'] = isset($arguments['convertErrorsToExceptions']) ? $arguments['convertErrorsToExceptions'] : true; $arguments['convertNoticesToExceptions'] = isset($arguments['convertNoticesToExceptions']) ? $arguments['convertNoticesToExceptions'] : true; $arguments['convertWarningsToExceptions'] = isset($arguments['convertWarningsToExceptions']) ? $arguments['convertWarningsToExceptions'] : true; $arguments['excludeGroups'] = isset($arguments['excludeGroups']) ? $arguments['excludeGroups'] : array(); $arguments['groups'] = isset($arguments['groups']) ? $arguments['groups'] : array(); $arguments['logIncompleteSkipped'] = isset($arguments['logIncompleteSkipped']) ? $arguments['logIncompleteSkipped'] : false; $arguments['processIsolation'] = isset($arguments['processIsolation']) ? $arguments['processIsolation'] : false; $arguments['repeat'] = isset($arguments['repeat']) ? $arguments['repeat'] : false; $arguments['reportHighLowerBound'] = isset($arguments['reportHighLowerBound']) ? $arguments['reportHighLowerBound'] : 90; $arguments['reportLowUpperBound'] = isset($arguments['reportLowUpperBound']) ? $arguments['reportLowUpperBound'] : 50; $arguments['crap4jThreshold'] = isset($arguments['crap4jThreshold']) ? $arguments['crap4jThreshold'] : 30; $arguments['stopOnError'] = isset($arguments['stopOnError']) ? $arguments['stopOnError'] : false; $arguments['stopOnFailure'] = isset($arguments['stopOnFailure']) ? $arguments['stopOnFailure'] : false; $arguments['stopOnIncomplete'] = isset($arguments['stopOnIncomplete']) ? $arguments['stopOnIncomplete'] : false; $arguments['stopOnRisky'] = isset($arguments['stopOnRisky']) ? $arguments['stopOnRisky'] : false; $arguments['stopOnSkipped'] = isset($arguments['stopOnSkipped']) ? $arguments['stopOnSkipped'] : false; $arguments['timeoutForSmallTests'] = isset($arguments['timeoutForSmallTests']) ? $arguments['timeoutForSmallTests'] : 1; $arguments['timeoutForMediumTests'] = isset($arguments['timeoutForMediumTests']) ? $arguments['timeoutForMediumTests'] : 10; $arguments['timeoutForLargeTests'] = isset($arguments['timeoutForLargeTests']) ? $arguments['timeoutForLargeTests'] : 60; $arguments['reportUselessTests'] = isset($arguments['reportUselessTests']) ? $arguments['reportUselessTests'] : false; $arguments['strictCoverage'] = isset($arguments['strictCoverage']) ? $arguments['strictCoverage'] : false; $arguments['disallowTestOutput'] = isset($arguments['disallowTestOutput']) ? $arguments['disallowTestOutput'] : false; $arguments['enforceTimeLimit'] = isset($arguments['enforceTimeLimit']) ? $arguments['enforceTimeLimit'] : false; $arguments['disallowTodoAnnotatedTests'] = isset($arguments['disallowTodoAnnotatedTests']) ? $arguments['disallowTodoAnnotatedTests'] : false; $arguments['verbose'] = isset($arguments['verbose']) ? $arguments['verbose'] : false; } /** * @param $extension * @param string $message * * @since Method available since Release 4.7.3 */ private function showExtensionNotLoadedWarning($extension, $message = '') { if (isset($this->missingExtensions[$extension])) { return; } $this->write("Warning:\t" . 'The ' . $extension . ' extension is not loaded' . "\n"); if (!empty($message)) { $this->write("\t\t" . $message . "\n"); } $this->missingExtensions[$extension] = true; } /** * @return PHP_CodeCoverage_Filter */ private function getCodeCoverageFilter() { $filter = new PHP_CodeCoverage_Filter; if (defined('__PHPUNIT_PHAR__')) { $filter->addFileToBlacklist(__PHPUNIT_PHAR__); } $blacklist = new PHPUnit_Util_Blacklist; foreach ($blacklist->getBlacklistedDirectories() as $directory) { $filter->addDirectoryToBlacklist($directory); } return $filter; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Utility class for blacklisting PHPUnit's own source code files. * * @since Class available since Release 4.0.0 */ class PHPUnit_Util_Blacklist { /** * @var array */ public static $blacklistedClassNames = array( 'File_Iterator' => 1, 'PHP_CodeCoverage' => 1, 'PHP_Invoker' => 1, 'PHP_Timer' => 1, 'PHP_Token' => 1, 'PHPUnit_Framework_TestCase' => 2, 'PHPUnit_Extensions_Database_TestCase' => 2, 'PHPUnit_Framework_MockObject_Generator' => 2, 'PHPUnit_Extensions_SeleniumTestCase' => 2, 'Text_Template' => 1, 'Symfony\Component\Yaml\Yaml' => 1, 'SebastianBergmann\Diff\Diff' => 1, 'SebastianBergmann\Environment\Runtime' => 1, 'SebastianBergmann\Comparator\Comparator' => 1, 'SebastianBergmann\Exporter\Exporter' => 1, 'SebastianBergmann\GlobalState\Snapshot' => 1, 'SebastianBergmann\RecursionContext\Context' => 1, 'SebastianBergmann\Version' => 1, 'Composer\Autoload\ClassLoader' => 1, 'Doctrine\Instantiator\Instantiator' => 1, 'phpDocumentor\Reflection\DocBlock' => 1, 'Prophecy\Prophet' => 1 ); /** * @var array */ private static $directories; /** * @return array * * @since Method available since Release 4.1.0 */ public function getBlacklistedDirectories() { $this->initialize(); return self::$directories; } /** * @param string $file * * @return bool */ public function isBlacklisted($file) { if (defined('PHPUNIT_TESTSUITE')) { return false; } $this->initialize(); foreach (self::$directories as $directory) { if (strpos($file, $directory) === 0) { return true; } } return false; } private function initialize() { if (self::$directories === null) { self::$directories = array(); foreach (self::$blacklistedClassNames as $className => $parent) { if (!class_exists($className)) { continue; } $reflector = new ReflectionClass($className); $directory = $reflector->getFileName(); for ($i = 0; $i < $parent; $i++) { $directory = dirname($directory); } self::$directories[] = $directory; } // Hide process isolation workaround on Windows. // @see PHPUnit_Util_PHP::factory() // @see PHPUnit_Util_PHP_Windows::process() if (DIRECTORY_SEPARATOR === '\\') { // tempnam() prefix is limited to first 3 chars. // @see http://php.net/manual/en/function.tempnam.php self::$directories[] = sys_get_temp_dir() . '\\PHP'; } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Wrapper for the PHPUnit XML configuration file. * * Example XML configuration file: * * * * * * * /path/to/files * /path/to/MyTest.php * /path/to/files/exclude * * * * * * name * * * name * * * * * * /path/to/files * /path/to/file * * /path/to/files * /path/to/file * * * * /path/to/files * /path/to/file * * /path/to/files * /path/to/file * * * * * * * * * * Sebastian * * * 22 * April * 19.78 * * * MyRelativeFile.php * MyRelativeDir * * * * * * * * * * * * * * * * * * . * * * * * * * * * * * * * * * * * * * @since Class available since Release 3.2.0 */ class PHPUnit_Util_Configuration { private static $instances = array(); protected $document; protected $xpath; protected $filename; /** * Loads a PHPUnit configuration file. * * @param string $filename */ protected function __construct($filename) { $this->filename = $filename; $this->document = PHPUnit_Util_XML::loadFile($filename, false, true, true); $this->xpath = new DOMXPath($this->document); } /** * @since Method available since Release 3.4.0 */ final private function __clone() { } /** * Returns a PHPUnit configuration object. * * @param string $filename * * @return PHPUnit_Util_Configuration * * @since Method available since Release 3.4.0 */ public static function getInstance($filename) { $realpath = realpath($filename); if ($realpath === false) { throw new PHPUnit_Framework_Exception( sprintf( 'Could not read "%s".', $filename ) ); } if (!isset(self::$instances[$realpath])) { self::$instances[$realpath] = new self($realpath); } return self::$instances[$realpath]; } /** * Returns the realpath to the configuration file. * * @return string * * @since Method available since Release 3.6.0 */ public function getFilename() { return $this->filename; } /** * Returns the configuration for SUT filtering. * * @return array * * @since Method available since Release 3.2.1 */ public function getFilterConfiguration() { $addUncoveredFilesFromWhitelist = true; $processUncoveredFilesFromWhitelist = false; $tmp = $this->xpath->query('filter/whitelist'); if ($tmp->length == 1) { if ($tmp->item(0)->hasAttribute('addUncoveredFilesFromWhitelist')) { $addUncoveredFilesFromWhitelist = $this->getBoolean( (string) $tmp->item(0)->getAttribute( 'addUncoveredFilesFromWhitelist' ), true ); } if ($tmp->item(0)->hasAttribute('processUncoveredFilesFromWhitelist')) { $processUncoveredFilesFromWhitelist = $this->getBoolean( (string) $tmp->item(0)->getAttribute( 'processUncoveredFilesFromWhitelist' ), false ); } } return array( 'blacklist' => array( 'include' => array( 'directory' => $this->readFilterDirectories( 'filter/blacklist/directory' ), 'file' => $this->readFilterFiles( 'filter/blacklist/file' ) ), 'exclude' => array( 'directory' => $this->readFilterDirectories( 'filter/blacklist/exclude/directory' ), 'file' => $this->readFilterFiles( 'filter/blacklist/exclude/file' ) ) ), 'whitelist' => array( 'addUncoveredFilesFromWhitelist' => $addUncoveredFilesFromWhitelist, 'processUncoveredFilesFromWhitelist' => $processUncoveredFilesFromWhitelist, 'include' => array( 'directory' => $this->readFilterDirectories( 'filter/whitelist/directory' ), 'file' => $this->readFilterFiles( 'filter/whitelist/file' ) ), 'exclude' => array( 'directory' => $this->readFilterDirectories( 'filter/whitelist/exclude/directory' ), 'file' => $this->readFilterFiles( 'filter/whitelist/exclude/file' ) ) ) ); } /** * Returns the configuration for groups. * * @return array * * @since Method available since Release 3.2.1 */ public function getGroupConfiguration() { $groups = array( 'include' => array(), 'exclude' => array() ); foreach ($this->xpath->query('groups/include/group') as $group) { $groups['include'][] = (string) $group->textContent; } foreach ($this->xpath->query('groups/exclude/group') as $group) { $groups['exclude'][] = (string) $group->textContent; } return $groups; } /** * Returns the configuration for listeners. * * @return array * * @since Method available since Release 3.4.0 */ public function getListenerConfiguration() { $result = array(); foreach ($this->xpath->query('listeners/listener') as $listener) { $class = (string) $listener->getAttribute('class'); $file = ''; $arguments = array(); if ($listener->getAttribute('file')) { $file = $this->toAbsolutePath( (string) $listener->getAttribute('file'), true ); } foreach ($listener->childNodes as $node) { if ($node instanceof DOMElement && $node->tagName == 'arguments') { foreach ($node->childNodes as $argument) { if ($argument instanceof DOMElement) { if ($argument->tagName == 'file' || $argument->tagName == 'directory') { $arguments[] = $this->toAbsolutePath((string) $argument->textContent); } else { $arguments[] = PHPUnit_Util_XML::xmlToVariable($argument); } } } } } $result[] = array( 'class' => $class, 'file' => $file, 'arguments' => $arguments ); } return $result; } /** * Returns the logging configuration. * * @return array */ public function getLoggingConfiguration() { $result = array(); foreach ($this->xpath->query('logging/log') as $log) { $type = (string) $log->getAttribute('type'); $target = (string) $log->getAttribute('target'); if (!$target) { continue; } $target = $this->toAbsolutePath($target); if ($type == 'coverage-html') { if ($log->hasAttribute('lowUpperBound')) { $result['lowUpperBound'] = $this->getInteger( (string) $log->getAttribute('lowUpperBound'), 50 ); } if ($log->hasAttribute('highLowerBound')) { $result['highLowerBound'] = $this->getInteger( (string) $log->getAttribute('highLowerBound'), 90 ); } } elseif ($type == 'coverage-crap4j') { if ($log->hasAttribute('threshold')) { $result['crap4jThreshold'] = $this->getInteger( (string) $log->getAttribute('threshold'), 30 ); } } elseif ($type == 'junit') { if ($log->hasAttribute('logIncompleteSkipped')) { $result['logIncompleteSkipped'] = $this->getBoolean( (string) $log->getAttribute('logIncompleteSkipped'), false ); } } elseif ($type == 'coverage-text') { if ($log->hasAttribute('showUncoveredFiles')) { $result['coverageTextShowUncoveredFiles'] = $this->getBoolean( (string) $log->getAttribute('showUncoveredFiles'), false ); } if ($log->hasAttribute('showOnlySummary')) { $result['coverageTextShowOnlySummary'] = $this->getBoolean( (string) $log->getAttribute('showOnlySummary'), false ); } } $result[$type] = $target; } return $result; } /** * Returns the PHP configuration. * * @return array * * @since Method available since Release 3.2.1 */ public function getPHPConfiguration() { $result = array( 'include_path' => array(), 'ini' => array(), 'const' => array(), 'var' => array(), 'env' => array(), 'post' => array(), 'get' => array(), 'cookie' => array(), 'server' => array(), 'files' => array(), 'request' => array() ); foreach ($this->xpath->query('php/includePath') as $includePath) { $path = (string) $includePath->textContent; if ($path) { $result['include_path'][] = $this->toAbsolutePath($path); } } foreach ($this->xpath->query('php/ini') as $ini) { $name = (string) $ini->getAttribute('name'); $value = (string) $ini->getAttribute('value'); $result['ini'][$name] = $value; } foreach ($this->xpath->query('php/const') as $const) { $name = (string) $const->getAttribute('name'); $value = (string) $const->getAttribute('value'); $result['const'][$name] = $this->getBoolean($value, $value); } foreach (array('var', 'env', 'post', 'get', 'cookie', 'server', 'files', 'request') as $array) { foreach ($this->xpath->query('php/' . $array) as $var) { $name = (string) $var->getAttribute('name'); $value = (string) $var->getAttribute('value'); $result[$array][$name] = $this->getBoolean($value, $value); } } return $result; } /** * Handles the PHP configuration. * * @since Method available since Release 3.2.20 */ public function handlePHPConfiguration() { $configuration = $this->getPHPConfiguration(); if (! empty($configuration['include_path'])) { ini_set( 'include_path', implode(PATH_SEPARATOR, $configuration['include_path']) . PATH_SEPARATOR . ini_get('include_path') ); } foreach ($configuration['ini'] as $name => $value) { if (defined($value)) { $value = constant($value); } ini_set($name, $value); } foreach ($configuration['const'] as $name => $value) { if (!defined($name)) { define($name, $value); } } foreach (array('var', 'post', 'get', 'cookie', 'server', 'files', 'request') as $array) { // See https://github.com/sebastianbergmann/phpunit/issues/277 switch ($array) { case 'var': $target = &$GLOBALS; break; case 'server': $target = &$_SERVER; break; default: $target = &$GLOBALS['_' . strtoupper($array)]; break; } foreach ($configuration[$array] as $name => $value) { $target[$name] = $value; } } foreach ($configuration['env'] as $name => $value) { if (false === getenv($name)) { putenv("{$name}={$value}"); } if (!isset($_ENV[$name])) { $_ENV[$name] = $value; } } } /** * Returns the PHPUnit configuration. * * @return array * * @since Method available since Release 3.2.14 */ public function getPHPUnitConfiguration() { $result = array(); $root = $this->document->documentElement; if ($root->hasAttribute('cacheTokens')) { $result['cacheTokens'] = $this->getBoolean( (string) $root->getAttribute('cacheTokens'), false ); } if ($root->hasAttribute('columns')) { $columns = (string) $root->getAttribute('columns'); if ($columns == 'max') { $result['columns'] = 'max'; } else { $result['columns'] = $this->getInteger($columns, 80); } } if ($root->hasAttribute('colors')) { /* only allow boolean for compatibility with previous versions 'always' only allowed from command line */ if ($this->getBoolean($root->getAttribute('colors'), false)) { $result['colors'] = PHPUnit_TextUI_ResultPrinter::COLOR_AUTO; } else { $result['colors'] = PHPUnit_TextUI_ResultPrinter::COLOR_NEVER; } } /* * Issue #657 */ if ($root->hasAttribute('stderr')) { $result['stderr'] = $this->getBoolean( (string) $root->getAttribute('stderr'), false ); } if ($root->hasAttribute('backupGlobals')) { $result['backupGlobals'] = $this->getBoolean( (string) $root->getAttribute('backupGlobals'), true ); } if ($root->hasAttribute('backupStaticAttributes')) { $result['backupStaticAttributes'] = $this->getBoolean( (string) $root->getAttribute('backupStaticAttributes'), false ); } if ($root->getAttribute('bootstrap')) { $result['bootstrap'] = $this->toAbsolutePath( (string) $root->getAttribute('bootstrap') ); } if ($root->hasAttribute('convertErrorsToExceptions')) { $result['convertErrorsToExceptions'] = $this->getBoolean( (string) $root->getAttribute('convertErrorsToExceptions'), true ); } if ($root->hasAttribute('convertNoticesToExceptions')) { $result['convertNoticesToExceptions'] = $this->getBoolean( (string) $root->getAttribute('convertNoticesToExceptions'), true ); } if ($root->hasAttribute('convertWarningsToExceptions')) { $result['convertWarningsToExceptions'] = $this->getBoolean( (string) $root->getAttribute('convertWarningsToExceptions'), true ); } if ($root->hasAttribute('forceCoversAnnotation')) { $result['forceCoversAnnotation'] = $this->getBoolean( (string) $root->getAttribute('forceCoversAnnotation'), false ); } if ($root->hasAttribute('mapTestClassNameToCoveredClassName')) { $result['mapTestClassNameToCoveredClassName'] = $this->getBoolean( (string) $root->getAttribute('mapTestClassNameToCoveredClassName'), false ); } if ($root->hasAttribute('processIsolation')) { $result['processIsolation'] = $this->getBoolean( (string) $root->getAttribute('processIsolation'), false ); } if ($root->hasAttribute('stopOnError')) { $result['stopOnError'] = $this->getBoolean( (string) $root->getAttribute('stopOnError'), false ); } if ($root->hasAttribute('stopOnFailure')) { $result['stopOnFailure'] = $this->getBoolean( (string) $root->getAttribute('stopOnFailure'), false ); } if ($root->hasAttribute('stopOnIncomplete')) { $result['stopOnIncomplete'] = $this->getBoolean( (string) $root->getAttribute('stopOnIncomplete'), false ); } if ($root->hasAttribute('stopOnRisky')) { $result['stopOnRisky'] = $this->getBoolean( (string) $root->getAttribute('stopOnRisky'), false ); } if ($root->hasAttribute('stopOnSkipped')) { $result['stopOnSkipped'] = $this->getBoolean( (string) $root->getAttribute('stopOnSkipped'), false ); } if ($root->hasAttribute('testSuiteLoaderClass')) { $result['testSuiteLoaderClass'] = (string) $root->getAttribute( 'testSuiteLoaderClass' ); } if ($root->getAttribute('testSuiteLoaderFile')) { $result['testSuiteLoaderFile'] = $this->toAbsolutePath( (string) $root->getAttribute('testSuiteLoaderFile') ); } if ($root->hasAttribute('printerClass')) { $result['printerClass'] = (string) $root->getAttribute( 'printerClass' ); } if ($root->getAttribute('printerFile')) { $result['printerFile'] = $this->toAbsolutePath( (string) $root->getAttribute('printerFile') ); } if ($root->hasAttribute('timeoutForSmallTests')) { $result['timeoutForSmallTests'] = $this->getInteger( (string) $root->getAttribute('timeoutForSmallTests'), 1 ); } if ($root->hasAttribute('timeoutForMediumTests')) { $result['timeoutForMediumTests'] = $this->getInteger( (string) $root->getAttribute('timeoutForMediumTests'), 10 ); } if ($root->hasAttribute('timeoutForLargeTests')) { $result['timeoutForLargeTests'] = $this->getInteger( (string) $root->getAttribute('timeoutForLargeTests'), 60 ); } if ($root->hasAttribute('beStrictAboutTestsThatDoNotTestAnything')) { $result['reportUselessTests'] = $this->getBoolean( (string) $root->getAttribute('beStrictAboutTestsThatDoNotTestAnything'), false ); } if ($root->hasAttribute('checkForUnintentionallyCoveredCode')) { $result['strictCoverage'] = $this->getBoolean( (string) $root->getAttribute('checkForUnintentionallyCoveredCode'), false ); } if ($root->hasAttribute('beStrictAboutOutputDuringTests')) { $result['disallowTestOutput'] = $this->getBoolean( (string) $root->getAttribute('beStrictAboutOutputDuringTests'), false ); } if ($root->hasAttribute('beStrictAboutChangesToGlobalState')) { $result['disallowChangesToGlobalState'] = $this->getBoolean( (string) $root->getAttribute('beStrictAboutChangesToGlobalState'), false ); } if ($root->hasAttribute('beStrictAboutTestSize')) { $result['enforceTimeLimit'] = $this->getBoolean( (string) $root->getAttribute('beStrictAboutTestSize'), false ); } if ($root->hasAttribute('beStrictAboutTodoAnnotatedTests')) { $result['disallowTodoAnnotatedTests'] = $this->getBoolean( (string) $root->getAttribute('beStrictAboutTodoAnnotatedTests'), false ); } if ($root->hasAttribute('strict')) { $flag = $this->getBoolean( (string) $root->getAttribute('strict'), false ); $result['reportUselessTests'] = $flag; $result['strictCoverage'] = $flag; $result['disallowTestOutput'] = $flag; $result['enforceTimeLimit'] = $flag; $result['disallowTodoAnnotatedTests'] = $flag; $result['deprecatedStrictModeSetting'] = true; } if ($root->hasAttribute('verbose')) { $result['verbose'] = $this->getBoolean( (string) $root->getAttribute('verbose'), false ); } return $result; } /** * Returns the SeleniumTestCase browser configuration. * * @return array * * @since Method available since Release 3.2.9 */ public function getSeleniumBrowserConfiguration() { $result = array(); foreach ($this->xpath->query('selenium/browser') as $config) { $name = (string) $config->getAttribute('name'); $browser = (string) $config->getAttribute('browser'); if ($config->hasAttribute('host')) { $host = (string) $config->getAttribute('host'); } else { $host = 'localhost'; } if ($config->hasAttribute('port')) { $port = $this->getInteger( (string) $config->getAttribute('port'), 4444 ); } else { $port = 4444; } if ($config->hasAttribute('timeout')) { $timeout = $this->getInteger( (string) $config->getAttribute('timeout'), 30000 ); } else { $timeout = 30000; } $result[] = array( 'name' => $name, 'browser' => $browser, 'host' => $host, 'port' => $port, 'timeout' => $timeout ); } return $result; } /** * Returns the test suite configuration. * * @return PHPUnit_Framework_TestSuite * * @since Method available since Release 3.2.1 */ public function getTestSuiteConfiguration($testSuiteFilter = null) { $testSuiteNodes = $this->xpath->query('testsuites/testsuite'); if ($testSuiteNodes->length == 0) { $testSuiteNodes = $this->xpath->query('testsuite'); } if ($testSuiteNodes->length == 1) { return $this->getTestSuite($testSuiteNodes->item(0), $testSuiteFilter); } if ($testSuiteNodes->length > 1) { $suite = new PHPUnit_Framework_TestSuite; foreach ($testSuiteNodes as $testSuiteNode) { $suite->addTestSuite( $this->getTestSuite($testSuiteNode, $testSuiteFilter) ); } return $suite; } } /** * @param DOMElement $testSuiteNode * * @return PHPUnit_Framework_TestSuite * * @since Method available since Release 3.4.0 */ protected function getTestSuite(DOMElement $testSuiteNode, $testSuiteFilter = null) { if ($testSuiteNode->hasAttribute('name')) { $suite = new PHPUnit_Framework_TestSuite( (string) $testSuiteNode->getAttribute('name') ); } else { $suite = new PHPUnit_Framework_TestSuite; } $exclude = array(); foreach ($testSuiteNode->getElementsByTagName('exclude') as $excludeNode) { $excludeFile = (string) $excludeNode->textContent; if ($excludeFile) { $exclude[] = $this->toAbsolutePath($excludeFile); } } $fileIteratorFacade = new File_Iterator_Facade; foreach ($testSuiteNode->getElementsByTagName('directory') as $directoryNode) { if ($testSuiteFilter && $directoryNode->parentNode->getAttribute('name') != $testSuiteFilter) { continue; } $directory = (string) $directoryNode->textContent; if (empty($directory)) { continue; } if ($directoryNode->hasAttribute('phpVersion')) { $phpVersion = (string) $directoryNode->getAttribute('phpVersion'); } else { $phpVersion = PHP_VERSION; } if ($directoryNode->hasAttribute('phpVersionOperator')) { $phpVersionOperator = (string) $directoryNode->getAttribute('phpVersionOperator'); } else { $phpVersionOperator = '>='; } if (!version_compare(PHP_VERSION, $phpVersion, $phpVersionOperator)) { continue; } if ($directoryNode->hasAttribute('prefix')) { $prefix = (string) $directoryNode->getAttribute('prefix'); } else { $prefix = ''; } if ($directoryNode->hasAttribute('suffix')) { $suffix = (string) $directoryNode->getAttribute('suffix'); } else { $suffix = 'Test.php'; } $files = $fileIteratorFacade->getFilesAsArray( $this->toAbsolutePath($directory), $suffix, $prefix, $exclude ); $suite->addTestFiles($files); } foreach ($testSuiteNode->getElementsByTagName('file') as $fileNode) { if ($testSuiteFilter && $fileNode->parentNode->getAttribute('name') != $testSuiteFilter) { continue; } $file = (string) $fileNode->textContent; if (empty($file)) { continue; } // Get the absolute path to the file $file = $fileIteratorFacade->getFilesAsArray( $this->toAbsolutePath($file) ); if (!isset($file[0])) { continue; } $file = $file[0]; if ($fileNode->hasAttribute('phpVersion')) { $phpVersion = (string) $fileNode->getAttribute('phpVersion'); } else { $phpVersion = PHP_VERSION; } if ($fileNode->hasAttribute('phpVersionOperator')) { $phpVersionOperator = (string) $fileNode->getAttribute('phpVersionOperator'); } else { $phpVersionOperator = '>='; } if (!version_compare(PHP_VERSION, $phpVersion, $phpVersionOperator)) { continue; } $suite->addTestFile($file); } return $suite; } /** * @param string $value * @param bool $default * * @return bool * * @since Method available since Release 3.2.3 */ protected function getBoolean($value, $default) { if (strtolower($value) == 'false') { return false; } elseif (strtolower($value) == 'true') { return true; } return $default; } /** * @param string $value * @param bool $default * * @return bool * * @since Method available since Release 3.6.0 */ protected function getInteger($value, $default) { if (is_numeric($value)) { return (int) $value; } return $default; } /** * @param string $query * * @return array * * @since Method available since Release 3.2.3 */ protected function readFilterDirectories($query) { $directories = array(); foreach ($this->xpath->query($query) as $directory) { $directoryPath = (string) $directory->textContent; if (!$directoryPath) { continue; } if ($directory->hasAttribute('prefix')) { $prefix = (string) $directory->getAttribute('prefix'); } else { $prefix = ''; } if ($directory->hasAttribute('suffix')) { $suffix = (string) $directory->getAttribute('suffix'); } else { $suffix = '.php'; } if ($directory->hasAttribute('group')) { $group = (string) $directory->getAttribute('group'); } else { $group = 'DEFAULT'; } $directories[] = array( 'path' => $this->toAbsolutePath($directoryPath), 'prefix' => $prefix, 'suffix' => $suffix, 'group' => $group ); } return $directories; } /** * @param string $query * * @return array * * @since Method available since Release 3.2.3 */ protected function readFilterFiles($query) { $files = array(); foreach ($this->xpath->query($query) as $file) { $filePath = (string) $file->textContent; if ($filePath) { $files[] = $this->toAbsolutePath($filePath); } } return $files; } /** * @param string $path * @param bool $useIncludePath * * @return string * * @since Method available since Release 3.5.0 */ protected function toAbsolutePath($path, $useIncludePath = false) { $path = trim($path); if ($path[0] === '/') { return $path; } // Matches the following on Windows: // - \\NetworkComputer\Path // - \\.\D: // - \\.\c: // - C:\Windows // - C:\windows // - C:/windows // - c:/windows if (defined('PHP_WINDOWS_VERSION_BUILD') && ($path[0] === '\\' || (strlen($path) >= 3 && preg_match('#^[A-Z]\:[/\\\]#i', substr($path, 0, 3))))) { return $path; } // Stream if (strpos($path, '://') !== false) { return $path; } $file = dirname($this->filename) . DIRECTORY_SEPARATOR . $path; if ($useIncludePath && !file_exists($file)) { $includePathFile = stream_resolve_include_path($path); if ($includePathFile) { $file = $includePathFile; } } return $file; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ // Workaround for http://bugs.php.net/bug.php?id=47987, // see https://github.com/sebastianbergmann/phpunit/issues#issue/125 for details // Use dirname(__DIR__) instead of using /../ because of https://github.com/facebook/hhvm/issues/5215 require_once dirname(__DIR__) . '/Framework/Error.php'; require_once dirname(__DIR__) . '/Framework/Error/Notice.php'; require_once dirname(__DIR__) . '/Framework/Error/Warning.php'; require_once dirname(__DIR__) . '/Framework/Error/Deprecated.php'; /** * Error handler that converts PHP errors and warnings to exceptions. * * @since Class available since Release 3.3.0 */ class PHPUnit_Util_ErrorHandler { protected static $errorStack = array(); /** * Returns the error stack. * * @return array */ public static function getErrorStack() { return self::$errorStack; } /** * @param int $errno * @param string $errstr * @param string $errfile * @param int $errline * * @throws PHPUnit_Framework_Error */ public static function handleError($errno, $errstr, $errfile, $errline) { if (!($errno & error_reporting())) { return false; } self::$errorStack[] = array($errno, $errstr, $errfile, $errline); $trace = debug_backtrace(false); array_shift($trace); foreach ($trace as $frame) { if ($frame['function'] == '__toString') { return false; } } if ($errno == E_NOTICE || $errno == E_USER_NOTICE || $errno == E_STRICT) { if (PHPUnit_Framework_Error_Notice::$enabled !== true) { return false; } $exception = 'PHPUnit_Framework_Error_Notice'; } elseif ($errno == E_WARNING || $errno == E_USER_WARNING) { if (PHPUnit_Framework_Error_Warning::$enabled !== true) { return false; } $exception = 'PHPUnit_Framework_Error_Warning'; } elseif ($errno == E_DEPRECATED || $errno == E_USER_DEPRECATED) { if (PHPUnit_Framework_Error_Deprecated::$enabled !== true) { return false; } $exception = 'PHPUnit_Framework_Error_Deprecated'; } else { $exception = 'PHPUnit_Framework_Error'; } throw new $exception($errstr, $errno, $errfile, $errline); } /** * Registers an error handler and returns a function that will restore * the previous handler when invoked * * @param int $severity PHP predefined error constant * * @throws Exception if event of specified severity is emitted */ public static function handleErrorOnce($severity = E_WARNING) { $terminator = function () { static $expired = false; if (!$expired) { $expired = true; // cleans temporary error handler return restore_error_handler(); } }; set_error_handler(function ($errno, $errstr) use ($severity) { if ($errno === $severity) { return; } return false; }); return $terminator; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Utility methods to load PHP sourcefiles. * * @since Class available since Release 2.3.0 */ class PHPUnit_Util_Fileloader { /** * Checks if a PHP sourcefile is readable. * The sourcefile is loaded through the load() method. * * @param string $filename * * @return string * * @throws PHPUnit_Framework_Exception */ public static function checkAndLoad($filename) { $includePathFilename = stream_resolve_include_path($filename); if (!$includePathFilename || !is_readable($includePathFilename)) { throw new PHPUnit_Framework_Exception( sprintf('Cannot open file "%s".' . "\n", $filename) ); } self::load($includePathFilename); return $includePathFilename; } /** * Loads a PHP sourcefile. * * @param string $filename * * @return mixed * * @since Method available since Release 3.0.0 */ public static function load($filename) { $oldVariableNames = array_keys(get_defined_vars()); include_once $filename; $newVariables = get_defined_vars(); $newVariableNames = array_diff( array_keys($newVariables), $oldVariableNames ); foreach ($newVariableNames as $variableName) { if ($variableName != 'oldVariableNames') { $GLOBALS[$variableName] = $newVariables[$variableName]; } } return $filename; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Filesystem helpers. * * @since Class available since Release 3.0.0 */ class PHPUnit_Util_Filesystem { /** * @var array */ protected static $buffer = array(); /** * Maps class names to source file names: * - PEAR CS: Foo_Bar_Baz -> Foo/Bar/Baz.php * - Namespace: Foo\Bar\Baz -> Foo/Bar/Baz.php * * @param string $className * * @return string * * @since Method available since Release 3.4.0 */ public static function classNameToFilename($className) { return str_replace( array('_', '\\'), DIRECTORY_SEPARATOR, $className ) . '.php'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Utility class for code filtering. * * @since Class available since Release 2.0.0 */ class PHPUnit_Util_Filter { /** * Filters stack frames from PHPUnit classes. * * @param Exception $e * @param bool $asString * * @return string */ public static function getFilteredStacktrace(Exception $e, $asString = true) { $prefix = false; $script = realpath($GLOBALS['_SERVER']['SCRIPT_NAME']); if (defined('__PHPUNIT_PHAR_ROOT__')) { $prefix = __PHPUNIT_PHAR_ROOT__; } if ($asString === true) { $filteredStacktrace = ''; } else { $filteredStacktrace = array(); } if ($e instanceof PHPUnit_Framework_SyntheticError) { $eTrace = $e->getSyntheticTrace(); $eFile = $e->getSyntheticFile(); $eLine = $e->getSyntheticLine(); } elseif ($e instanceof PHPUnit_Framework_Exception) { $eTrace = $e->getSerializableTrace(); $eFile = $e->getFile(); $eLine = $e->getLine(); } else { if ($e->getPrevious()) { $e = $e->getPrevious(); } $eTrace = $e->getTrace(); $eFile = $e->getFile(); $eLine = $e->getLine(); } if (!self::frameExists($eTrace, $eFile, $eLine)) { array_unshift( $eTrace, array('file' => $eFile, 'line' => $eLine) ); } $blacklist = new PHPUnit_Util_Blacklist; foreach ($eTrace as $frame) { if (isset($frame['file']) && is_file($frame['file']) && !$blacklist->isBlacklisted($frame['file']) && ($prefix === false || strpos($frame['file'], $prefix) !== 0) && $frame['file'] !== $script) { if ($asString === true) { $filteredStacktrace .= sprintf( "%s:%s\n", $frame['file'], isset($frame['line']) ? $frame['line'] : '?' ); } else { $filteredStacktrace[] = $frame; } } } return $filteredStacktrace; } /** * @param array $trace * @param string $file * @param int $line * * @return bool * * @since Method available since Release 3.3.2 */ private static function frameExists(array $trace, $file, $line) { foreach ($trace as $frame) { if (isset($frame['file']) && $frame['file'] == $file && isset($frame['line']) && $frame['line'] == $line) { return true; } } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Command-line options parsing class. * * @since Class available since Release 3.0.0 */ class PHPUnit_Util_Getopt { public static function getopt(array $args, $short_options, $long_options = null) { if (empty($args)) { return array(array(), array()); } $opts = array(); $non_opts = array(); if ($long_options) { sort($long_options); } if (isset($args[0][0]) && $args[0][0] != '-') { array_shift($args); } reset($args); array_map('trim', $args); while (list($i, $arg) = each($args)) { if ($arg == '') { continue; } if ($arg == '--') { $non_opts = array_merge($non_opts, array_slice($args, $i + 1)); break; } if ($arg[0] != '-' || (strlen($arg) > 1 && $arg[1] == '-' && !$long_options)) { $non_opts[] = $args[$i]; continue; } elseif (strlen($arg) > 1 && $arg[1] == '-') { self::parseLongOption( substr($arg, 2), $long_options, $opts, $args ); } else { self::parseShortOption( substr($arg, 1), $short_options, $opts, $args ); } } return array($opts, $non_opts); } protected static function parseShortOption($arg, $short_options, &$opts, &$args) { $argLen = strlen($arg); for ($i = 0; $i < $argLen; $i++) { $opt = $arg[$i]; $opt_arg = null; if (($spec = strstr($short_options, $opt)) === false || $arg[$i] == ':') { throw new PHPUnit_Framework_Exception( "unrecognized option -- $opt" ); } if (strlen($spec) > 1 && $spec[1] == ':') { if (strlen($spec) > 2 && $spec[2] == ':') { if ($i + 1 < $argLen) { $opts[] = array($opt, substr($arg, $i + 1)); break; } } else { if ($i + 1 < $argLen) { $opts[] = array($opt, substr($arg, $i + 1)); break; } elseif (list(, $opt_arg) = each($args)) { } else { throw new PHPUnit_Framework_Exception( "option requires an argument -- $opt" ); } } } $opts[] = array($opt, $opt_arg); } } protected static function parseLongOption($arg, $long_options, &$opts, &$args) { $count = count($long_options); $list = explode('=', $arg); $opt = $list[0]; $opt_arg = null; if (count($list) > 1) { $opt_arg = $list[1]; } $opt_len = strlen($opt); for ($i = 0; $i < $count; $i++) { $long_opt = $long_options[$i]; $opt_start = substr($long_opt, 0, $opt_len); if ($opt_start != $opt) { continue; } $opt_rest = substr($long_opt, $opt_len); if ($opt_rest != '' && $opt[0] != '=' && $i + 1 < $count && $opt == substr($long_options[$i+1], 0, $opt_len)) { throw new PHPUnit_Framework_Exception( "option --$opt is ambiguous" ); } if (substr($long_opt, -1) == '=') { if (substr($long_opt, -2) != '==') { if (!strlen($opt_arg) && !(list(, $opt_arg) = each($args))) { throw new PHPUnit_Framework_Exception( "option --$opt requires an argument" ); } } } elseif ($opt_arg) { throw new PHPUnit_Framework_Exception( "option --$opt doesn't allow an argument" ); } $full_option = '--' . preg_replace('/={1,2}$/', '', $long_opt); $opts[] = array($full_option, $opt_arg); return; } throw new PHPUnit_Framework_Exception("unrecognized option --$opt"); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 3.4.0 */ class PHPUnit_Util_GlobalState { /** * @var array */ protected static $superGlobalArrays = array( '_ENV', '_POST', '_GET', '_COOKIE', '_SERVER', '_FILES', '_REQUEST' ); /** * @var array */ protected static $superGlobalArraysLong = array( 'HTTP_ENV_VARS', 'HTTP_POST_VARS', 'HTTP_GET_VARS', 'HTTP_COOKIE_VARS', 'HTTP_SERVER_VARS', 'HTTP_POST_FILES' ); public static function getIncludedFilesAsString() { return static::processIncludedFilesAsString(get_included_files()); } public static function processIncludedFilesAsString(array $files) { $blacklist = new PHPUnit_Util_Blacklist; $prefix = false; $result = ''; if (defined('__PHPUNIT_PHAR__')) { $prefix = 'phar://' . __PHPUNIT_PHAR__ . '/'; } for ($i = count($files) - 1; $i > 0; $i--) { $file = $files[$i]; if ($prefix !== false && strpos($file, $prefix) === 0) { continue; } // Skip virtual file system protocols if (preg_match('/^(vfs|phpvfs[a-z0-9]+):/', $file)) { continue; } if (!$blacklist->isBlacklisted($file) && is_file($file)) { $result = 'require_once \'' . $file . "';\n" . $result; } } return $result; } public static function getIniSettingsAsString() { $result = ''; $iniSettings = ini_get_all(null, false); foreach ($iniSettings as $key => $value) { $result .= sprintf( '@ini_set(%s, %s);' . "\n", self::exportVariable($key), self::exportVariable($value) ); } return $result; } public static function getConstantsAsString() { $constants = get_defined_constants(true); $result = ''; if (isset($constants['user'])) { foreach ($constants['user'] as $name => $value) { $result .= sprintf( 'if (!defined(\'%s\')) define(\'%s\', %s);' . "\n", $name, $name, self::exportVariable($value) ); } } return $result; } public static function getGlobalsAsString() { $result = ''; $superGlobalArrays = self::getSuperGlobalArrays(); foreach ($superGlobalArrays as $superGlobalArray) { if (isset($GLOBALS[$superGlobalArray]) && is_array($GLOBALS[$superGlobalArray])) { foreach (array_keys($GLOBALS[$superGlobalArray]) as $key) { if ($GLOBALS[$superGlobalArray][$key] instanceof Closure) { continue; } $result .= sprintf( '$GLOBALS[\'%s\'][\'%s\'] = %s;' . "\n", $superGlobalArray, $key, self::exportVariable($GLOBALS[$superGlobalArray][$key]) ); } } } $blacklist = $superGlobalArrays; $blacklist[] = 'GLOBALS'; foreach (array_keys($GLOBALS) as $key) { if (!in_array($key, $blacklist) && !$GLOBALS[$key] instanceof Closure) { $result .= sprintf( '$GLOBALS[\'%s\'] = %s;' . "\n", $key, self::exportVariable($GLOBALS[$key]) ); } } return $result; } protected static function getSuperGlobalArrays() { if (ini_get('register_long_arrays') == '1') { return array_merge( self::$superGlobalArrays, self::$superGlobalArraysLong ); } else { return self::$superGlobalArrays; } } protected static function exportVariable($variable) { if (is_scalar($variable) || is_null($variable) || (is_array($variable) && self::arrayOnlyContainsScalars($variable))) { return var_export($variable, true); } return 'unserialize(' . var_export(serialize($variable), true) . ')'; } protected static function arrayOnlyContainsScalars(array $array) { $result = true; foreach ($array as $element) { if (is_array($element)) { $result = self::arrayOnlyContainsScalars($element); } elseif (!is_scalar($element) && !is_null($element)) { $result = false; } if ($result === false) { break; } } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Factory for PHPUnit_Framework_Exception objects that are used to describe * invalid arguments passed to a function or method. * * @since Class available since Release 3.4.0 */ class PHPUnit_Util_InvalidArgumentHelper { /** * @param int $argument * @param string $type * @param mixed $value * * @return PHPUnit_Framework_Exception */ public static function factory($argument, $type, $value = null) { $stack = debug_backtrace(false); return new PHPUnit_Framework_Exception( sprintf( 'Argument #%d%sof %s::%s() must be a %s', $argument, $value !== null ? ' (' . gettype($value) . '#' . $value . ')' : ' (No Value) ', $stack[1]['class'], $stack[1]['function'], $type ) ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A TestListener that generates JSON messages. * * @since Class available since Release 3.0.0 */ class PHPUnit_Util_Log_JSON extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener { /** * @var string */ protected $currentTestSuiteName = ''; /** * @var string */ protected $currentTestName = ''; /** * @var bool */ protected $currentTestPass = true; /** * An error occurred. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->writeCase( 'error', $time, PHPUnit_Util_Filter::getFilteredStacktrace($e, false), $e->getMessage(), $test ); $this->currentTestPass = false; } /** * A failure occurred. * * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_AssertionFailedError $e * @param float $time */ public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { $this->writeCase( 'fail', $time, PHPUnit_Util_Filter::getFilteredStacktrace($e, false), $e->getMessage(), $test ); $this->currentTestPass = false; } /** * Incomplete test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->writeCase( 'error', $time, PHPUnit_Util_Filter::getFilteredStacktrace($e, false), 'Incomplete Test: ' . $e->getMessage(), $test ); $this->currentTestPass = false; } /** * Risky test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 4.0.0 */ public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->writeCase( 'error', $time, PHPUnit_Util_Filter::getFilteredStacktrace($e, false), 'Risky Test: ' . $e->getMessage(), $test ); $this->currentTestPass = false; } /** * Skipped test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->writeCase( 'error', $time, PHPUnit_Util_Filter::getFilteredStacktrace($e, false), 'Skipped Test: ' . $e->getMessage(), $test ); $this->currentTestPass = false; } /** * A testsuite started. * * @param PHPUnit_Framework_TestSuite $suite */ public function startTestSuite(PHPUnit_Framework_TestSuite $suite) { $this->currentTestSuiteName = $suite->getName(); $this->currentTestName = ''; $this->write( array( 'event' => 'suiteStart', 'suite' => $this->currentTestSuiteName, 'tests' => count($suite) ) ); } /** * A testsuite ended. * * @param PHPUnit_Framework_TestSuite $suite */ public function endTestSuite(PHPUnit_Framework_TestSuite $suite) { $this->currentTestSuiteName = ''; $this->currentTestName = ''; } /** * A test started. * * @param PHPUnit_Framework_Test $test */ public function startTest(PHPUnit_Framework_Test $test) { $this->currentTestName = PHPUnit_Util_Test::describe($test); $this->currentTestPass = true; $this->write( array( 'event' => 'testStart', 'suite' => $this->currentTestSuiteName, 'test' => $this->currentTestName ) ); } /** * A test ended. * * @param PHPUnit_Framework_Test $test * @param float $time */ public function endTest(PHPUnit_Framework_Test $test, $time) { if ($this->currentTestPass) { $this->writeCase('pass', $time, array(), '', $test); } } /** * @param string $status * @param float $time * @param array $trace * @param string $message * @param PHPUnit_Framework_TestCase|null $test */ protected function writeCase($status, $time, array $trace = array(), $message = '', $test = null) { $output = ''; // take care of TestSuite producing error (e.g. by running into exception) as TestSuite doesn't have hasOutput if ($test !== null && method_exists($test, 'hasOutput') && $test->hasOutput()) { $output = $test->getActualOutput(); } $this->write( array( 'event' => 'test', 'suite' => $this->currentTestSuiteName, 'test' => $this->currentTestName, 'status' => $status, 'time' => $time, 'trace' => $trace, 'message' => PHPUnit_Util_String::convertToUtf8($message), 'output' => $output, ) ); } /** * @param string $buffer */ public function write($buffer) { array_walk_recursive($buffer, function (&$input) { if (is_string($input)) { $input = PHPUnit_Util_String::convertToUtf8($input); } }); $flags = 0; if (defined('JSON_PRETTY_PRINT')) { $flags |= JSON_PRETTY_PRINT; } parent::write(json_encode($buffer, $flags)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A TestListener that generates a logfile of the test execution in XML markup. * * The XML markup used is the same as the one that is used by the JUnit Ant task. * * @since Class available since Release 2.1.0 */ class PHPUnit_Util_Log_JUnit extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener { /** * @var DOMDocument */ protected $document; /** * @var DOMElement */ protected $root; /** * @var bool */ protected $logIncompleteSkipped = false; /** * @var bool */ protected $writeDocument = true; /** * @var DOMElement[] */ protected $testSuites = array(); /** * @var int[] */ protected $testSuiteTests = array(0); /** * @var int[] */ protected $testSuiteAssertions = array(0); /** * @var int[] */ protected $testSuiteErrors = array(0); /** * @var int[] */ protected $testSuiteFailures = array(0); /** * @var int[] */ protected $testSuiteTimes = array(0); /** * @var int */ protected $testSuiteLevel = 0; /** * @var DOMElement */ protected $currentTestCase = null; /** * @var bool */ protected $attachCurrentTestCase = true; /** * Constructor. * * @param mixed $out * @param bool $logIncompleteSkipped */ public function __construct($out = null, $logIncompleteSkipped = false) { $this->document = new DOMDocument('1.0', 'UTF-8'); $this->document->formatOutput = true; $this->root = $this->document->createElement('testsuites'); $this->document->appendChild($this->root); parent::__construct($out); $this->logIncompleteSkipped = $logIncompleteSkipped; } /** * Flush buffer and close output. */ public function flush() { if ($this->writeDocument === true) { $this->write($this->getXML()); } parent::flush(); } /** * An error occurred. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { if ($this->currentTestCase === null) { return; } if ($test instanceof PHPUnit_Framework_SelfDescribing) { $buffer = $test->toString() . "\n"; } else { $buffer = ''; } $buffer .= PHPUnit_Framework_TestFailure::exceptionToString($e) . "\n" . PHPUnit_Util_Filter::getFilteredStacktrace($e); $error = $this->document->createElement( 'error', PHPUnit_Util_XML::prepareString($buffer) ); $error->setAttribute('type', get_class($e)); $this->currentTestCase->appendChild($error); $this->testSuiteErrors[$this->testSuiteLevel]++; } /** * A failure occurred. * * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_AssertionFailedError $e * @param float $time */ public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { if ($this->currentTestCase === null) { return; } if ($test instanceof PHPUnit_Framework_SelfDescribing) { $buffer = $test->toString() . "\n"; } else { $buffer = ''; } $buffer .= PHPUnit_Framework_TestFailure::exceptionToString($e) . "\n" . PHPUnit_Util_Filter::getFilteredStacktrace($e); $failure = $this->document->createElement( 'failure', PHPUnit_Util_XML::prepareString($buffer) ); $failure->setAttribute('type', get_class($e)); $this->currentTestCase->appendChild($failure); $this->testSuiteFailures[$this->testSuiteLevel]++; } /** * Incomplete test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { if ($this->logIncompleteSkipped && $this->currentTestCase !== null) { $error = $this->document->createElement( 'error', PHPUnit_Util_XML::prepareString( "Incomplete Test\n" . PHPUnit_Util_Filter::getFilteredStacktrace($e) ) ); $error->setAttribute('type', get_class($e)); $this->currentTestCase->appendChild($error); $this->testSuiteErrors[$this->testSuiteLevel]++; } else { $this->attachCurrentTestCase = false; } } /** * Risky test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 4.0.0 */ public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { if ($this->logIncompleteSkipped && $this->currentTestCase !== null) { $error = $this->document->createElement( 'error', PHPUnit_Util_XML::prepareString( "Risky Test\n" . PHPUnit_Util_Filter::getFilteredStacktrace($e) ) ); $error->setAttribute('type', get_class($e)); $this->currentTestCase->appendChild($error); $this->testSuiteErrors[$this->testSuiteLevel]++; } else { $this->attachCurrentTestCase = false; } } /** * Skipped test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 3.0.0 */ public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { if ($this->logIncompleteSkipped && $this->currentTestCase !== null) { $error = $this->document->createElement( 'error', PHPUnit_Util_XML::prepareString( "Skipped Test\n" . PHPUnit_Util_Filter::getFilteredStacktrace($e) ) ); $error->setAttribute('type', get_class($e)); $this->currentTestCase->appendChild($error); $this->testSuiteErrors[$this->testSuiteLevel]++; } else { $this->attachCurrentTestCase = false; } } /** * A testsuite started. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function startTestSuite(PHPUnit_Framework_TestSuite $suite) { $testSuite = $this->document->createElement('testsuite'); $testSuite->setAttribute('name', $suite->getName()); if (class_exists($suite->getName(), false)) { try { $class = new ReflectionClass($suite->getName()); $testSuite->setAttribute('file', $class->getFileName()); } catch (ReflectionException $e) { } } if ($this->testSuiteLevel > 0) { $this->testSuites[$this->testSuiteLevel]->appendChild($testSuite); } else { $this->root->appendChild($testSuite); } $this->testSuiteLevel++; $this->testSuites[$this->testSuiteLevel] = $testSuite; $this->testSuiteTests[$this->testSuiteLevel] = 0; $this->testSuiteAssertions[$this->testSuiteLevel] = 0; $this->testSuiteErrors[$this->testSuiteLevel] = 0; $this->testSuiteFailures[$this->testSuiteLevel] = 0; $this->testSuiteTimes[$this->testSuiteLevel] = 0; } /** * A testsuite ended. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function endTestSuite(PHPUnit_Framework_TestSuite $suite) { $this->testSuites[$this->testSuiteLevel]->setAttribute( 'tests', $this->testSuiteTests[$this->testSuiteLevel] ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'assertions', $this->testSuiteAssertions[$this->testSuiteLevel] ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'failures', $this->testSuiteFailures[$this->testSuiteLevel] ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'errors', $this->testSuiteErrors[$this->testSuiteLevel] ); $this->testSuites[$this->testSuiteLevel]->setAttribute( 'time', sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]) ); if ($this->testSuiteLevel > 1) { $this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel]; $this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel]; $this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel]; $this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel]; $this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel]; } $this->testSuiteLevel--; } /** * A test started. * * @param PHPUnit_Framework_Test $test */ public function startTest(PHPUnit_Framework_Test $test) { $testCase = $this->document->createElement('testcase'); $testCase->setAttribute('name', $test->getName()); if ($test instanceof PHPUnit_Framework_TestCase) { $class = new ReflectionClass($test); $methodName = $test->getName(); if ($class->hasMethod($methodName)) { $method = $class->getMethod($test->getName()); $testCase->setAttribute('class', $class->getName()); $testCase->setAttribute('file', $class->getFileName()); $testCase->setAttribute('line', $method->getStartLine()); } } $this->currentTestCase = $testCase; } /** * A test ended. * * @param PHPUnit_Framework_Test $test * @param float $time */ public function endTest(PHPUnit_Framework_Test $test, $time) { if ($this->attachCurrentTestCase) { if ($test instanceof PHPUnit_Framework_TestCase) { $numAssertions = $test->getNumAssertions(); $this->testSuiteAssertions[$this->testSuiteLevel] += $numAssertions; $this->currentTestCase->setAttribute( 'assertions', $numAssertions ); } $this->currentTestCase->setAttribute( 'time', sprintf('%F', $time) ); $this->testSuites[$this->testSuiteLevel]->appendChild( $this->currentTestCase ); $this->testSuiteTests[$this->testSuiteLevel]++; $this->testSuiteTimes[$this->testSuiteLevel] += $time; if (method_exists($test, 'hasOutput') && $test->hasOutput()) { $systemOut = $this->document->createElement('system-out'); $systemOut->appendChild( $this->document->createTextNode($test->getActualOutput()) ); $this->currentTestCase->appendChild($systemOut); } } $this->attachCurrentTestCase = true; $this->currentTestCase = null; } /** * Returns the XML as a string. * * @return string * * @since Method available since Release 2.2.0 */ public function getXML() { return $this->document->saveXML(); } /** * Enables or disables the writing of the document * in flush(). * * This is a "hack" needed for the integration of * PHPUnit with Phing. * * @return string * * @since Method available since Release 2.2.0 */ public function setWriteDocument($flag) { if (is_bool($flag)) { $this->writeDocument = $flag; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * A TestListener that generates a logfile of the * test execution using the Test Anything Protocol (TAP). * * @since Class available since Release 3.0.0 */ class PHPUnit_Util_Log_TAP extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener { /** * @var int */ protected $testNumber = 0; /** * @var int */ protected $testSuiteLevel = 0; /** * @var bool */ protected $testSuccessful = true; /** * Constructor. * * @param mixed $out * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.3.4 */ public function __construct($out = null) { parent::__construct($out); $this->write("TAP version 13\n"); } /** * An error occurred. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->writeNotOk($test, 'Error'); } /** * A failure occurred. * * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_AssertionFailedError $e * @param float $time */ public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { $this->writeNotOk($test, 'Failure'); $message = explode( "\n", PHPUnit_Framework_TestFailure::exceptionToString($e) ); $diagnostic = array( 'message' => $message[0], 'severity' => 'fail' ); if ($e instanceof PHPUnit_Framework_ExpectationFailedException) { $cf = $e->getComparisonFailure(); if ($cf !== null) { $diagnostic['data'] = array( 'got' => $cf->getActual(), 'expected' => $cf->getExpected() ); } } $yaml = new Symfony\Component\Yaml\Dumper; $this->write( sprintf( " ---\n%s ...\n", $yaml->dump($diagnostic, 2, 2) ) ); } /** * Incomplete test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->writeNotOk($test, '', 'TODO Incomplete Test'); } /** * Risky test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 4.0.0 */ public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->write( sprintf( "ok %d - # RISKY%s\n", $this->testNumber, $e->getMessage() != '' ? ' ' . $e->getMessage() : '' ) ); $this->testSuccessful = false; } /** * Skipped test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 3.0.0 */ public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { $this->write( sprintf( "ok %d - # SKIP%s\n", $this->testNumber, $e->getMessage() != '' ? ' ' . $e->getMessage() : '' ) ); $this->testSuccessful = false; } /** * A testsuite started. * * @param PHPUnit_Framework_TestSuite $suite */ public function startTestSuite(PHPUnit_Framework_TestSuite $suite) { $this->testSuiteLevel++; } /** * A testsuite ended. * * @param PHPUnit_Framework_TestSuite $suite */ public function endTestSuite(PHPUnit_Framework_TestSuite $suite) { $this->testSuiteLevel--; if ($this->testSuiteLevel == 0) { $this->write(sprintf("1..%d\n", $this->testNumber)); } } /** * A test started. * * @param PHPUnit_Framework_Test $test */ public function startTest(PHPUnit_Framework_Test $test) { $this->testNumber++; $this->testSuccessful = true; } /** * A test ended. * * @param PHPUnit_Framework_Test $test * @param float $time */ public function endTest(PHPUnit_Framework_Test $test, $time) { if ($this->testSuccessful === true) { $this->write( sprintf( "ok %d - %s\n", $this->testNumber, PHPUnit_Util_Test::describe($test) ) ); } $this->writeDiagnostics($test); } /** * @param PHPUnit_Framework_Test $test * @param string $prefix * @param string $directive */ protected function writeNotOk(PHPUnit_Framework_Test $test, $prefix = '', $directive = '') { $this->write( sprintf( "not ok %d - %s%s%s\n", $this->testNumber, $prefix != '' ? $prefix . ': ' : '', PHPUnit_Util_Test::describe($test), $directive != '' ? ' # ' . $directive : '' ) ); $this->testSuccessful = false; } /** * @param PHPUnit_Framework_Test $test */ private function writeDiagnostics(PHPUnit_Framework_Test $test) { if (!$test instanceof PHPUnit_Framework_TestCase) { return; } if (!$test->hasOutput()) { return; } foreach (explode("\n", trim($test->getActualOutput())) as $line) { $this->write( sprintf( "# %s\n", $line ) ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Environment\Runtime; /** * Default utility for PHP sub-processes. * * @since Class available since Release 3.5.12 */ class PHPUnit_Util_PHP_Default extends PHPUnit_Util_PHP { /** * Runs a single job (PHP code) using a separate PHP process. * * @param string $job * @param array $settings * * @return array * * @throws PHPUnit_Framework_Exception */ public function runJob($job, array $settings = array()) { $runtime = new Runtime; $runtime = $runtime->getBinary() . $this->settingsToParameters($settings); if ('phpdbg' === PHP_SAPI) { $runtime .= ' -qrr ' . escapeshellarg(__DIR__ . '/eval-stdin.php'); } $process = proc_open( $runtime, array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w') ), $pipes ); if (!is_resource($process)) { throw new PHPUnit_Framework_Exception( 'Unable to spawn worker process' ); } $this->process($pipes[0], $job); fclose($pipes[0]); $stdout = stream_get_contents($pipes[1]); fclose($pipes[1]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); proc_close($process); $this->cleanup(); return array('stdout' => $stdout, 'stderr' => $stderr); } /** * @param resource $pipe * @param string $job * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.5.12 */ protected function process($pipe, $job) { fwrite($pipe, $job); } /** * @since Method available since Release 3.5.12 */ protected function cleanup() { } } ' . file_get_contents('php://stdin')); * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Environment\Runtime; /** * Windows utility for PHP sub-processes. * * @since Class available since Release 3.5.12 */ class PHPUnit_Util_PHP_Windows extends PHPUnit_Util_PHP_Default { /** * @var string */ private $tempFile; /** * {@inheritdoc} * * Reading from STDOUT or STDERR hangs forever on Windows if the output is * too large. * * @see https://bugs.php.net/bug.php?id=51800 */ public function runJob($job, array $settings = array()) { $runtime = new Runtime; if (false === $stdout_handle = tmpfile()) { throw new PHPUnit_Framework_Exception( 'A temporary file could not be created; verify that your TEMP environment variable is writable' ); } $process = proc_open( $runtime->getBinary() . $this->settingsToParameters($settings), array( 0 => array('pipe', 'r'), 1 => $stdout_handle, 2 => array('pipe', 'w') ), $pipes ); if (!is_resource($process)) { throw new PHPUnit_Framework_Exception( 'Unable to spawn worker process' ); } $this->process($pipes[0], $job); fclose($pipes[0]); $stderr = stream_get_contents($pipes[2]); fclose($pipes[2]); proc_close($process); rewind($stdout_handle); $stdout = stream_get_contents($stdout_handle); fclose($stdout_handle); $this->cleanup(); return array('stdout' => $stdout, 'stderr' => $stderr); } /** * @param resource $pipe * @param string $job * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.5.12 */ protected function process($pipe, $job) { if (!($this->tempFile = tempnam(sys_get_temp_dir(), 'PHPUnit')) || file_put_contents($this->tempFile, $job) === false) { throw new PHPUnit_Framework_Exception( 'Unable to write temporary file' ); } fwrite( $pipe, 'tempFile, true) . '; ?>' ); } /** * @since Method available since Release 3.5.12 */ protected function cleanup() { unlink($this->tempFile); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Utility methods for PHP sub-processes. * * @since Class available since Release 3.4.0 */ abstract class PHPUnit_Util_PHP { /** * @return PHPUnit_Util_PHP * * @since Method available since Release 3.5.12 */ public static function factory() { if (DIRECTORY_SEPARATOR == '\\') { return new PHPUnit_Util_PHP_Windows; } return new PHPUnit_Util_PHP_Default; } /** * Runs a single test in a separate PHP process. * * @param string $job * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_TestResult $result * * @throws PHPUnit_Framework_Exception */ public function runTestJob($job, PHPUnit_Framework_Test $test, PHPUnit_Framework_TestResult $result) { $result->startTest($test); $_result = $this->runJob($job); $this->processChildResult( $test, $result, $_result['stdout'], $_result['stderr'] ); } /** * Runs a single job (PHP code) using a separate PHP process. * * @param string $job * @param array $settings * * @return array * * @throws PHPUnit_Framework_Exception */ abstract public function runJob($job, array $settings = array()); /** * @param array $settings * * @return string * * @since Method available since Release 4.0.0 */ protected function settingsToParameters(array $settings) { $buffer = ''; foreach ($settings as $setting) { $buffer .= ' -d ' . $setting; } return $buffer; } /** * Processes the TestResult object from an isolated process. * * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_TestResult $result * @param string $stdout * @param string $stderr * * @since Method available since Release 3.5.0 */ private function processChildResult(PHPUnit_Framework_Test $test, PHPUnit_Framework_TestResult $result, $stdout, $stderr) { $time = 0; if (!empty($stderr)) { $result->addError( $test, new PHPUnit_Framework_Exception(trim($stderr)), $time ); } else { set_error_handler(function ($errno, $errstr, $errfile, $errline) { throw new ErrorException($errstr, $errno, $errno, $errfile, $errline); }); try { if (strpos($stdout, "#!/usr/bin/env php\n") === 0) { $stdout = substr($stdout, 19); } $childResult = unserialize(str_replace("#!/usr/bin/env php\n", '', $stdout)); restore_error_handler(); } catch (ErrorException $e) { restore_error_handler(); $childResult = false; $result->addError( $test, new PHPUnit_Framework_Exception(trim($stdout), 0, $e), $time ); } if ($childResult !== false) { if (!empty($childResult['output'])) { $output = $childResult['output']; } $test->setResult($childResult['testResult']); $test->addToAssertionCount($childResult['numAssertions']); $childResult = $childResult['result']; if ($result->getCollectCodeCoverageInformation()) { $result->getCodeCoverage()->merge( $childResult->getCodeCoverage() ); } $time = $childResult->time(); $notImplemented = $childResult->notImplemented(); $risky = $childResult->risky(); $skipped = $childResult->skipped(); $errors = $childResult->errors(); $failures = $childResult->failures(); if (!empty($notImplemented)) { $result->addError( $test, $this->getException($notImplemented[0]), $time ); } elseif (!empty($risky)) { $result->addError( $test, $this->getException($risky[0]), $time ); } elseif (!empty($skipped)) { $result->addError( $test, $this->getException($skipped[0]), $time ); } elseif (!empty($errors)) { $result->addError( $test, $this->getException($errors[0]), $time ); } elseif (!empty($failures)) { $result->addFailure( $test, $this->getException($failures[0]), $time ); } } } $result->endTest($test, $time); if (!empty($output)) { print $output; } } /** * Gets the thrown exception from a PHPUnit_Framework_TestFailure. * * @param PHPUnit_Framework_TestFailure $error * * @return Exception * * @since Method available since Release 3.6.0 * @see https://github.com/sebastianbergmann/phpunit/issues/74 */ private function getException(PHPUnit_Framework_TestFailure $error) { $exception = $error->thrownException(); if ($exception instanceof __PHP_Incomplete_Class) { $exceptionArray = array(); foreach ((array) $exception as $key => $value) { $key = substr($key, strrpos($key, "\0") + 1); $exceptionArray[$key] = $value; } $exception = new PHPUnit_Framework_SyntheticError( sprintf( '%s: %s', $exceptionArray['_PHP_Incomplete_Class_Name'], $exceptionArray['message'] ), $exceptionArray['code'], $exceptionArray['file'], $exceptionArray['line'], $exceptionArray['trace'] ); } return $exception; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Utility class that can print to STDOUT or write to a file. * * @since Class available since Release 2.0.0 */ class PHPUnit_Util_Printer { /** * If true, flush output after every write. * * @var bool */ protected $autoFlush = false; /** * @var resource */ protected $out; /** * @var string */ protected $outTarget; /** * @var bool */ protected $printsHTML = false; /** * Constructor. * * @param mixed $out * * @throws PHPUnit_Framework_Exception */ public function __construct($out = null) { if ($out !== null) { if (is_string($out)) { if (strpos($out, 'socket://') === 0) { $out = explode(':', str_replace('socket://', '', $out)); if (sizeof($out) != 2) { throw new PHPUnit_Framework_Exception; } $this->out = fsockopen($out[0], $out[1]); } else { if (strpos($out, 'php://') === false && !is_dir(dirname($out))) { mkdir(dirname($out), 0777, true); } $this->out = fopen($out, 'wt'); } $this->outTarget = $out; } else { $this->out = $out; } } } /** * Flush buffer, optionally tidy up HTML, and close output if it's not to a php stream */ public function flush() { if ($this->out && strncmp($this->outTarget, 'php://', 6) !== 0) { fclose($this->out); } if ($this->printsHTML === true && $this->outTarget !== null && strpos($this->outTarget, 'php://') !== 0 && strpos($this->outTarget, 'socket://') !== 0 && extension_loaded('tidy')) { file_put_contents( $this->outTarget, tidy_repair_file( $this->outTarget, array('indent' => true, 'wrap' => 0), 'utf8' ) ); } } /** * Performs a safe, incremental flush. * * Do not confuse this function with the flush() function of this class, * since the flush() function may close the file being written to, rendering * the current object no longer usable. * * @since Method available since Release 3.3.0 */ public function incrementalFlush() { if ($this->out) { fflush($this->out); } else { flush(); } } /** * @param string $buffer */ public function write($buffer) { if ($this->out) { fwrite($this->out, $buffer); if ($this->autoFlush) { $this->incrementalFlush(); } } else { if (PHP_SAPI != 'cli' && PHP_SAPI != 'phpdbg') { $buffer = htmlspecialchars($buffer); } print $buffer; if ($this->autoFlush) { $this->incrementalFlush(); } } } /** * Check auto-flush mode. * * @return bool * * @since Method available since Release 3.3.0 */ public function getAutoFlush() { return $this->autoFlush; } /** * Set auto-flushing mode. * * If set, *incremental* flushes will be done after each write. This should * not be confused with the different effects of this class' flush() method. * * @param bool $autoFlush * * @since Method available since Release 3.3.0 */ public function setAutoFlush($autoFlush) { if (is_bool($autoFlush)) { $this->autoFlush = $autoFlush; } else { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'boolean'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Error handler that converts PHP errors and warnings to exceptions. * * @since Class available since Release 4.2.0 */ class PHPUnit_Util_Regex { public static function pregMatchSafe($pattern, $subject, $matches = null, $flags = 0, $offset = 0) { $handler_terminator = PHPUnit_Util_ErrorHandler::handleErrorOnce(E_WARNING); $match = preg_match($pattern, $subject, $matches, $flags, $offset); $handler_terminator(); // cleaning return $match; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * String helpers. * * @since Class available since Release 3.6.0 */ class PHPUnit_Util_String { /** * Converts a string to UTF-8 encoding. * * @param string $string * * @return string */ public static function convertToUtf8($string) { if (!self::isUtf8($string)) { if (function_exists('mb_convert_encoding')) { $string = mb_convert_encoding($string, 'UTF-8'); } else { $string = utf8_encode($string); } } return $string; } /** * Checks a string for UTF-8 encoding. * * @param string $string * * @return bool */ protected static function isUtf8($string) { $length = strlen($string); for ($i = 0; $i < $length; $i++) { if (ord($string[$i]) < 0x80) { $n = 0; } elseif ((ord($string[$i]) & 0xE0) == 0xC0) { $n = 1; } elseif ((ord($string[$i]) & 0xF0) == 0xE0) { $n = 2; } elseif ((ord($string[$i]) & 0xF0) == 0xF0) { $n = 3; } else { return false; } for ($j = 0; $j < $n; $j++) { if ((++$i == $length) || ((ord($string[$i]) & 0xC0) != 0x80)) { return false; } } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ if (!function_exists('trait_exists')) { function trait_exists($traitname, $autoload = true) { return false; } } /** * Test helpers. * * @since Class available since Release 3.0.0 */ class PHPUnit_Util_Test { const REGEX_DATA_PROVIDER = '/@dataProvider\s+([a-zA-Z0-9._:-\\\\x7f-\xff]+)/'; const REGEX_TEST_WITH = '/@testWith\s+/'; const REGEX_EXPECTED_EXCEPTION = '(@expectedException\s+([:.\w\\\\x7f-\xff]+)(?:[\t ]+(\S*))?(?:[\t ]+(\S*))?\s*$)m'; const REGEX_REQUIRES_VERSION = '/@requires\s+(?PPHP(?:Unit)?)\s+(?P[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m'; const REGEX_REQUIRES_OS = '/@requires\s+OS\s+(?P.+?)[ \t]*\r?$/m'; const REGEX_REQUIRES = '/@requires\s+(?Pfunction|extension)\s+(?P([^ ]+?))[ \t]*\r?$/m'; const UNKNOWN = -1; const SMALL = 0; const MEDIUM = 1; const LARGE = 2; private static $annotationCache = array(); private static $hookMethods = array(); /** * @param PHPUnit_Framework_Test $test * @param bool $asString * * @return mixed */ public static function describe(PHPUnit_Framework_Test $test, $asString = true) { if ($asString) { if ($test instanceof PHPUnit_Framework_SelfDescribing) { return $test->toString(); } else { return get_class($test); } } else { if ($test instanceof PHPUnit_Framework_TestCase) { return array( get_class($test), $test->getName() ); } elseif ($test instanceof PHPUnit_Framework_SelfDescribing) { return array('', $test->toString()); } else { return array('', get_class($test)); } } } /** * @param string $className * @param string $methodName * * @return array|bool * * @throws PHPUnit_Framework_CodeCoverageException * * @since Method available since Release 4.0.0 */ public static function getLinesToBeCovered($className, $methodName) { $annotations = self::parseTestMethodAnnotations( $className, $methodName ); if (isset($annotations['class']['coversNothing']) || isset($annotations['method']['coversNothing'])) { return false; } return self::getLinesToBeCoveredOrUsed($className, $methodName, 'covers'); } /** * Returns lines of code specified with the @uses annotation. * * @param string $className * @param string $methodName * * @return array * * @since Method available since Release 4.0.0 */ public static function getLinesToBeUsed($className, $methodName) { return self::getLinesToBeCoveredOrUsed($className, $methodName, 'uses'); } /** * @param string $className * @param string $methodName * @param string $mode * * @return array * * @throws PHPUnit_Framework_CodeCoverageException * * @since Method available since Release 4.2.0 */ private static function getLinesToBeCoveredOrUsed($className, $methodName, $mode) { $annotations = self::parseTestMethodAnnotations( $className, $methodName ); $classShortcut = null; if (!empty($annotations['class'][$mode . 'DefaultClass'])) { if (count($annotations['class'][$mode . 'DefaultClass']) > 1) { throw new PHPUnit_Framework_CodeCoverageException( sprintf( 'More than one @%sClass annotation in class or interface "%s".', $mode, $className ) ); } $classShortcut = $annotations['class'][$mode . 'DefaultClass'][0]; } $list = array(); if (isset($annotations['class'][$mode])) { $list = $annotations['class'][$mode]; } if (isset($annotations['method'][$mode])) { $list = array_merge($list, $annotations['method'][$mode]); } $codeList = array(); foreach (array_unique($list) as $element) { if ($classShortcut && strncmp($element, '::', 2) === 0) { $element = $classShortcut . $element; } $element = preg_replace('/[\s()]+$/', '', $element); $element = explode(' ', $element); $element = $element[0]; $codeList = array_merge( $codeList, self::resolveElementToReflectionObjects($element) ); } return self::resolveReflectionObjectsToLines($codeList); } /** * Returns the requirements for a test. * * @param string $className * @param string $methodName * * @return array * * @since Method available since Release 3.6.0 */ public static function getRequirements($className, $methodName) { $reflector = new ReflectionClass($className); $docComment = $reflector->getDocComment(); $reflector = new ReflectionMethod($className, $methodName); $docComment .= "\n" . $reflector->getDocComment(); $requires = array(); if ($count = preg_match_all(self::REGEX_REQUIRES_OS, $docComment, $matches)) { $requires['OS'] = sprintf( '/%s/i', addcslashes($matches['value'][$count - 1], '/') ); } if ($count = preg_match_all(self::REGEX_REQUIRES_VERSION, $docComment, $matches)) { for ($i = 0; $i < $count; $i++) { $requires[$matches['name'][$i]] = $matches['value'][$i]; } } // https://bugs.php.net/bug.php?id=63055 $matches = array(); if ($count = preg_match_all(self::REGEX_REQUIRES, $docComment, $matches)) { for ($i = 0; $i < $count; $i++) { $name = $matches['name'][$i] . 's'; if (!isset($requires[$name])) { $requires[$name] = array(); } $requires[$name][] = $matches['value'][$i]; } } return $requires; } /** * Returns the missing requirements for a test. * * @param string $className * @param string $methodName * * @return array * * @since Method available since Release 4.3.0 */ public static function getMissingRequirements($className, $methodName) { $required = static::getRequirements($className, $methodName); $missing = array(); if (!empty($required['PHP']) && version_compare(PHP_VERSION, $required['PHP'], '<')) { $missing[] = sprintf('PHP %s (or later) is required.', $required['PHP']); } if (!empty($required['PHPUnit'])) { $phpunitVersion = PHPUnit_Runner_Version::id(); if (version_compare($phpunitVersion, $required['PHPUnit'], '<')) { $missing[] = sprintf('PHPUnit %s (or later) is required.', $required['PHPUnit']); } } if (!empty($required['OS']) && !preg_match($required['OS'], PHP_OS)) { $missing[] = sprintf('Operating system matching %s is required.', $required['OS']); } if (!empty($required['functions'])) { foreach ($required['functions'] as $function) { $pieces = explode('::', $function); if (2 === count($pieces) && method_exists($pieces[0], $pieces[1])) { continue; } if (function_exists($function)) { continue; } $missing[] = sprintf('Function %s is required.', $function); } } if (!empty($required['extensions'])) { foreach ($required['extensions'] as $extension) { if (!extension_loaded($extension)) { $missing[] = sprintf('Extension %s is required.', $extension); } } } return $missing; } /** * Returns the expected exception for a test. * * @param string $className * @param string $methodName * * @return array * * @since Method available since Release 3.3.6 */ public static function getExpectedException($className, $methodName) { $reflector = new ReflectionMethod($className, $methodName); $docComment = $reflector->getDocComment(); $docComment = substr($docComment, 3, -2); if (preg_match(self::REGEX_EXPECTED_EXCEPTION, $docComment, $matches)) { $annotations = self::parseTestMethodAnnotations( $className, $methodName ); $class = $matches[1]; $code = null; $message = ''; $messageRegExp = ''; if (isset($matches[2])) { $message = trim($matches[2]); } elseif (isset($annotations['method']['expectedExceptionMessage'])) { $message = self::parseAnnotationContent( $annotations['method']['expectedExceptionMessage'][0] ); } if (isset($annotations['method']['expectedExceptionMessageRegExp'])) { $messageRegExp = self::parseAnnotationContent( $annotations['method']['expectedExceptionMessageRegExp'][0] ); } if (isset($matches[3])) { $code = $matches[3]; } elseif (isset($annotations['method']['expectedExceptionCode'])) { $code = self::parseAnnotationContent( $annotations['method']['expectedExceptionCode'][0] ); } if (is_numeric($code)) { $code = (int) $code; } elseif (is_string($code) && defined($code)) { $code = (int) constant($code); } return array( 'class' => $class, 'code' => $code, 'message' => $message, 'message_regex' => $messageRegExp ); } return false; } /** * Parse annotation content to use constant/class constant values * * Constants are specified using a starting '@'. For example: @ClassName::CONST_NAME * * If the constant is not found the string is used as is to ensure maximum BC. * * @param string $message * * @return string */ private static function parseAnnotationContent($message) { if (strpos($message, '::') !== false && count(explode('::', $message)) == 2) { if (defined($message)) { $message = constant($message); } } return $message; } /** * Returns the provided data for a method. * * @param string $className * @param string $methodName * * @return array|Iterator when a data provider is specified and exists * null when no data provider is specified * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.2.0 */ public static function getProvidedData($className, $methodName) { $reflector = new ReflectionMethod($className, $methodName); $docComment = $reflector->getDocComment(); $data = null; if ($dataProviderData = self::getDataFromDataProviderAnnotation($docComment, $className, $methodName)) { $data = $dataProviderData; } if ($testWithData = self::getDataFromTestWithAnnotation($docComment)) { $data = $testWithData; } if ($data !== null) { if (is_object($data)) { $data = iterator_to_array($data); } foreach ($data as $key => $value) { if (!is_array($value)) { throw new PHPUnit_Framework_Exception( sprintf( 'Data set %s is invalid.', is_int($key) ? '#' . $key : '"' . $key . '"' ) ); } } } return $data; } /** * Returns the provided data for a method. * * @param string $docComment * @param string $className * @param string $methodName * * @return array|Iterator when a data provider is specified and exists * null when no data provider is specified * * @throws PHPUnit_Framework_Exception */ private static function getDataFromDataProviderAnnotation($docComment, $className, $methodName) { if (preg_match(self::REGEX_DATA_PROVIDER, $docComment, $matches)) { $dataProviderMethodNameNamespace = explode('\\', $matches[1]); $leaf = explode('::', array_pop($dataProviderMethodNameNamespace)); $dataProviderMethodName = array_pop($leaf); if (!empty($dataProviderMethodNameNamespace)) { $dataProviderMethodNameNamespace = implode('\\', $dataProviderMethodNameNamespace) . '\\'; } else { $dataProviderMethodNameNamespace = ''; } if (!empty($leaf)) { $dataProviderClassName = $dataProviderMethodNameNamespace . array_pop($leaf); } else { $dataProviderClassName = $className; } $dataProviderClass = new ReflectionClass($dataProviderClassName); $dataProviderMethod = $dataProviderClass->getMethod( $dataProviderMethodName ); if ($dataProviderMethod->isStatic()) { $object = null; } else { $object = $dataProviderClass->newInstance(); } if ($dataProviderMethod->getNumberOfParameters() == 0) { $data = $dataProviderMethod->invoke($object); } else { $data = $dataProviderMethod->invoke($object, $methodName); } return $data; } } /** * @param string $docComment full docComment string * * @return array when @testWith annotation is defined * null when @testWith annotation is omitted * * @throws PHPUnit_Framework_Exception when @testWith annotation is defined but cannot be parsed */ public static function getDataFromTestWithAnnotation($docComment) { $docComment = self::cleanUpMultiLineAnnotation($docComment); if (preg_match(self::REGEX_TEST_WITH, $docComment, $matches, PREG_OFFSET_CAPTURE)) { $offset = strlen($matches[0][0]) + $matches[0][1]; $annotationContent = substr($docComment, $offset); $data = array(); foreach (explode("\n", $annotationContent) as $candidateRow) { $candidateRow = trim($candidateRow); if ($candidateRow[0] !== '[') { break; } $dataSet = json_decode($candidateRow, true); if (json_last_error() != JSON_ERROR_NONE) { $error = function_exists('json_last_error_msg') ? json_last_error_msg() : json_last_error(); throw new PHPUnit_Framework_Exception( 'The dataset for the @testWith annotation cannot be parsed: ' . $error ); } $data[] = $dataSet; } if (!$data) { throw new PHPUnit_Framework_Exception('The dataset for the @testWith annotation cannot be parsed.'); } return $data; } } private static function cleanUpMultiLineAnnotation($docComment) { //removing initial ' * ' for docComment $docComment = preg_replace('/' . '\n' . '\s*' . '\*' . '\s?' . '/', "\n", $docComment); $docComment = substr($docComment, 0, -1); $docComment = rtrim($docComment, "\n"); return $docComment; } /** * @param string $className * @param string $methodName * * @return array * * @throws ReflectionException * * @since Method available since Release 3.4.0 */ public static function parseTestMethodAnnotations($className, $methodName = '') { if (!isset(self::$annotationCache[$className])) { $class = new ReflectionClass($className); self::$annotationCache[$className] = self::parseAnnotations($class->getDocComment()); } if (!empty($methodName) && !isset(self::$annotationCache[$className . '::' . $methodName])) { try { $method = new ReflectionMethod($className, $methodName); $annotations = self::parseAnnotations($method->getDocComment()); } catch (ReflectionException $e) { $annotations = array(); } self::$annotationCache[$className . '::' . $methodName] = $annotations; } return array( 'class' => self::$annotationCache[$className], 'method' => !empty($methodName) ? self::$annotationCache[$className . '::' . $methodName] : array() ); } /** * @param string $docblock * * @return array * * @since Method available since Release 3.4.0 */ private static function parseAnnotations($docblock) { $annotations = array(); // Strip away the docblock header and footer to ease parsing of one line annotations $docblock = substr($docblock, 3, -2); if (preg_match_all('/@(?P[A-Za-z_-]+)(?:[ \t]+(?P.*?))?[ \t]*\r?$/m', $docblock, $matches)) { $numMatches = count($matches[0]); for ($i = 0; $i < $numMatches; ++$i) { $annotations[$matches['name'][$i]][] = $matches['value'][$i]; } } return $annotations; } /** * Returns the backup settings for a test. * * @param string $className * @param string $methodName * * @return array * * @since Method available since Release 3.4.0 */ public static function getBackupSettings($className, $methodName) { return array( 'backupGlobals' => self::getBooleanAnnotationSetting( $className, $methodName, 'backupGlobals' ), 'backupStaticAttributes' => self::getBooleanAnnotationSetting( $className, $methodName, 'backupStaticAttributes' ) ); } /** * Returns the dependencies for a test class or method. * * @param string $className * @param string $methodName * * @return array * * @since Method available since Release 3.4.0 */ public static function getDependencies($className, $methodName) { $annotations = self::parseTestMethodAnnotations( $className, $methodName ); $dependencies = array(); if (isset($annotations['class']['depends'])) { $dependencies = $annotations['class']['depends']; } if (isset($annotations['method']['depends'])) { $dependencies = array_merge( $dependencies, $annotations['method']['depends'] ); } return array_unique($dependencies); } /** * Returns the error handler settings for a test. * * @param string $className * @param string $methodName * * @return bool * * @since Method available since Release 3.4.0 */ public static function getErrorHandlerSettings($className, $methodName) { return self::getBooleanAnnotationSetting( $className, $methodName, 'errorHandler' ); } /** * Returns the groups for a test class or method. * * @param string $className * @param string $methodName * * @return array * * @since Method available since Release 3.2.0 */ public static function getGroups($className, $methodName = '') { $annotations = self::parseTestMethodAnnotations( $className, $methodName ); $groups = array(); if (isset($annotations['method']['author'])) { $groups = $annotations['method']['author']; } elseif (isset($annotations['class']['author'])) { $groups = $annotations['class']['author']; } if (isset($annotations['class']['group'])) { $groups = array_merge($groups, $annotations['class']['group']); } if (isset($annotations['method']['group'])) { $groups = array_merge($groups, $annotations['method']['group']); } if (isset($annotations['class']['ticket'])) { $groups = array_merge($groups, $annotations['class']['ticket']); } if (isset($annotations['method']['ticket'])) { $groups = array_merge($groups, $annotations['method']['ticket']); } foreach (array('method', 'class') as $element) { foreach (array('small', 'medium', 'large') as $size) { if (isset($annotations[$element][$size])) { $groups[] = $size; break 2; } if (isset($annotations[$element][$size])) { $groups[] = $size; break 2; } } } return array_unique($groups); } /** * Returns the size of the test. * * @param string $className * @param string $methodName * * @return int * * @since Method available since Release 3.6.0 */ public static function getSize($className, $methodName) { $groups = array_flip(self::getGroups($className, $methodName)); $size = self::UNKNOWN; $class = new ReflectionClass($className); if (isset($groups['large']) || (class_exists('PHPUnit_Extensions_Database_TestCase', false) && $class->isSubclassOf('PHPUnit_Extensions_Database_TestCase')) || (class_exists('PHPUnit_Extensions_SeleniumTestCase', false) && $class->isSubclassOf('PHPUnit_Extensions_SeleniumTestCase'))) { $size = self::LARGE; } elseif (isset($groups['medium'])) { $size = self::MEDIUM; } elseif (isset($groups['small'])) { $size = self::SMALL; } return $size; } /** * Returns the tickets for a test class or method. * * @param string $className * @param string $methodName * * @return array * * @since Method available since Release 3.4.0 */ public static function getTickets($className, $methodName) { $annotations = self::parseTestMethodAnnotations( $className, $methodName ); $tickets = array(); if (isset($annotations['class']['ticket'])) { $tickets = $annotations['class']['ticket']; } if (isset($annotations['method']['ticket'])) { $tickets = array_merge($tickets, $annotations['method']['ticket']); } return array_unique($tickets); } /** * Returns the process isolation settings for a test. * * @param string $className * @param string $methodName * * @return bool * * @since Method available since Release 3.4.1 */ public static function getProcessIsolationSettings($className, $methodName) { $annotations = self::parseTestMethodAnnotations( $className, $methodName ); if (isset($annotations['class']['runTestsInSeparateProcesses']) || isset($annotations['method']['runInSeparateProcess'])) { return true; } else { return false; } } /** * Returns the preserve global state settings for a test. * * @param string $className * @param string $methodName * * @return bool * * @since Method available since Release 3.4.0 */ public static function getPreserveGlobalStateSettings($className, $methodName) { return self::getBooleanAnnotationSetting( $className, $methodName, 'preserveGlobalState' ); } /** * @param string $className * * @return array * * @since Method available since Release 4.0.8 */ public static function getHookMethods($className) { if (!class_exists($className, false)) { return self::emptyHookMethodsArray(); } if (!isset(self::$hookMethods[$className])) { self::$hookMethods[$className] = self::emptyHookMethodsArray(); try { $class = new ReflectionClass($className); foreach ($class->getMethods() as $method) { if (self::isBeforeClassMethod($method)) { self::$hookMethods[$className]['beforeClass'][] = $method->getName(); } if (self::isBeforeMethod($method)) { self::$hookMethods[$className]['before'][] = $method->getName(); } if (self::isAfterMethod($method)) { self::$hookMethods[$className]['after'][] = $method->getName(); } if (self::isAfterClassMethod($method)) { self::$hookMethods[$className]['afterClass'][] = $method->getName(); } } } catch (ReflectionException $e) { } } return self::$hookMethods[$className]; } /** * @return array * * @since Method available since Release 4.0.9 */ private static function emptyHookMethodsArray() { return array( 'beforeClass' => array('setUpBeforeClass'), 'before' => array('setUp'), 'after' => array('tearDown'), 'afterClass' => array('tearDownAfterClass') ); } /** * @param string $className * @param string $methodName * @param string $settingName * * @return bool * * @since Method available since Release 3.4.0 */ private static function getBooleanAnnotationSetting($className, $methodName, $settingName) { $annotations = self::parseTestMethodAnnotations( $className, $methodName ); $result = null; if (isset($annotations['class'][$settingName])) { if ($annotations['class'][$settingName][0] == 'enabled') { $result = true; } elseif ($annotations['class'][$settingName][0] == 'disabled') { $result = false; } } if (isset($annotations['method'][$settingName])) { if ($annotations['method'][$settingName][0] == 'enabled') { $result = true; } elseif ($annotations['method'][$settingName][0] == 'disabled') { $result = false; } } return $result; } /** * @param string $element * * @return array * * @throws PHPUnit_Framework_InvalidCoversTargetException * * @since Method available since Release 4.0.0 */ private static function resolveElementToReflectionObjects($element) { $codeToCoverList = array(); if (strpos($element, '\\') !== false && function_exists($element)) { $codeToCoverList[] = new ReflectionFunction($element); } elseif (strpos($element, '::') !== false) { list($className, $methodName) = explode('::', $element); if (isset($methodName[0]) && $methodName[0] == '<') { $classes = array($className); foreach ($classes as $className) { if (!class_exists($className) && !interface_exists($className)) { throw new PHPUnit_Framework_InvalidCoversTargetException( sprintf( 'Trying to @cover or @use not existing class or ' . 'interface "%s".', $className ) ); } $class = new ReflectionClass($className); $methods = $class->getMethods(); $inverse = isset($methodName[1]) && $methodName[1] == '!'; if (strpos($methodName, 'protected')) { $visibility = 'isProtected'; } elseif (strpos($methodName, 'private')) { $visibility = 'isPrivate'; } elseif (strpos($methodName, 'public')) { $visibility = 'isPublic'; } foreach ($methods as $method) { if ($inverse && !$method->$visibility()) { $codeToCoverList[] = $method; } elseif (!$inverse && $method->$visibility()) { $codeToCoverList[] = $method; } } } } else { $classes = array($className); foreach ($classes as $className) { if ($className == '' && function_exists($methodName)) { $codeToCoverList[] = new ReflectionFunction( $methodName ); } else { if (!((class_exists($className) || interface_exists($className) || trait_exists($className)) && method_exists($className, $methodName))) { throw new PHPUnit_Framework_InvalidCoversTargetException( sprintf( 'Trying to @cover or @use not existing method "%s::%s".', $className, $methodName ) ); } $codeToCoverList[] = new ReflectionMethod( $className, $methodName ); } } } } else { $extended = false; if (strpos($element, '') !== false) { $element = str_replace('', '', $element); $extended = true; } $classes = array($element); if ($extended) { $classes = array_merge( $classes, class_implements($element), class_parents($element) ); } foreach ($classes as $className) { if (!class_exists($className) && !interface_exists($className) && !trait_exists($className)) { throw new PHPUnit_Framework_InvalidCoversTargetException( sprintf( 'Trying to @cover or @use not existing class or ' . 'interface "%s".', $className ) ); } $codeToCoverList[] = new ReflectionClass($className); } } return $codeToCoverList; } /** * @param array $reflectors * * @return array */ private static function resolveReflectionObjectsToLines(array $reflectors) { $result = array(); foreach ($reflectors as $reflector) { $filename = $reflector->getFileName(); if (!isset($result[$filename])) { $result[$filename] = array(); } $result[$filename] = array_unique( array_merge( $result[$filename], range($reflector->getStartLine(), $reflector->getEndLine()) ) ); } return $result; } /** * @param ReflectionMethod $method * * @return bool * * @since Method available since Release 4.0.8 */ private static function isBeforeClassMethod(ReflectionMethod $method) { return $method->isStatic() && strpos($method->getDocComment(), '@beforeClass') !== false; } /** * @param ReflectionMethod $method * * @return bool * * @since Method available since Release 4.0.8 */ private static function isBeforeMethod(ReflectionMethod $method) { return preg_match('/@before\b/', $method->getDocComment()); } /** * @param ReflectionMethod $method * * @return bool * * @since Method available since Release 4.0.8 */ private static function isAfterClassMethod(ReflectionMethod $method) { return $method->isStatic() && strpos($method->getDocComment(), '@afterClass') !== false; } /** * @param ReflectionMethod $method * * @return bool * * @since Method available since Release 4.0.8 */ private static function isAfterMethod(ReflectionMethod $method) { return preg_match('/@after\b/', $method->getDocComment()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Prettifies class and method names for use in TestDox documentation. * * @since Class available since Release 2.1.0 */ class PHPUnit_Util_TestDox_NamePrettifier { /** * @var string */ protected $prefix = 'Test'; /** * @var string */ protected $suffix = 'Test'; /** * @var array */ protected $strings = array(); /** * Prettifies the name of a test class. * * @param string $name * * @return string */ public function prettifyTestClass($name) { $title = $name; if ($this->suffix !== null && $this->suffix == substr($name, -1 * strlen($this->suffix))) { $title = substr($title, 0, strripos($title, $this->suffix)); } if ($this->prefix !== null && $this->prefix == substr($name, 0, strlen($this->prefix))) { $title = substr($title, strlen($this->prefix)); } if (substr($title, 0, 1) == '\\') { $title = substr($title, 1); } return $title; } /** * Prettifies the name of a test method. * * @param string $name * * @return string */ public function prettifyTestMethod($name) { $buffer = ''; if (!is_string($name) || strlen($name) == 0) { return $buffer; } $string = preg_replace('#\d+$#', '', $name, -1, $count); if (in_array($string, $this->strings)) { $name = $string; } elseif ($count == 0) { $this->strings[] = $string; } if (substr($name, 0, 4) == 'test') { $name = substr($name, 4); } $name[0] = strtoupper($name[0]); if (strpos($name, '_') !== false) { return trim(str_replace('_', ' ', $name)); } $max = strlen($name); $wasNumeric = false; for ($i = 0; $i < $max; $i++) { if ($i > 0 && ord($name[$i]) >= 65 && ord($name[$i]) <= 90) { $buffer .= ' ' . strtolower($name[$i]); } else { $isNumeric = is_numeric($name[$i]); if (!$wasNumeric && $isNumeric) { $buffer .= ' '; $wasNumeric = true; } if ($wasNumeric && !$isNumeric) { $wasNumeric = false; } $buffer .= $name[$i]; } } return $buffer; } /** * Sets the prefix of test names. * * @param string $prefix */ public function setPrefix($prefix) { $this->prefix = $prefix; } /** * Sets the suffix of test names. * * @param string $suffix */ public function setSuffix($suffix) { $this->suffix = $suffix; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Prints TestDox documentation in HTML format. * * @since Class available since Release 2.1.0 */ class PHPUnit_Util_TestDox_ResultPrinter_HTML extends PHPUnit_Util_TestDox_ResultPrinter { /** * @var bool */ protected $printsHTML = true; /** * Handler for 'start run' event. */ protected function startRun() { $this->write(''); } /** * Handler for 'start class' event. * * @param string $name */ protected function startClass($name) { $this->write( '

    ' . $this->currentTestClassPrettified . '

      ' ); } /** * Handler for 'on test' event. * * @param string $name * @param bool $success */ protected function onTest($name, $success = true) { if (!$success) { $strikeOpen = ''; $strikeClose = ''; } else { $strikeOpen = ''; $strikeClose = ''; } $this->write('
    • ' . $strikeOpen . $name . $strikeClose . '
    • '); } /** * Handler for 'end class' event. * * @param string $name */ protected function endClass($name) { $this->write('
    '); } /** * Handler for 'end run' event. */ protected function endRun() { $this->write(''); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Prints TestDox documentation in text format. * * @since Class available since Release 2.1.0 */ class PHPUnit_Util_TestDox_ResultPrinter_Text extends PHPUnit_Util_TestDox_ResultPrinter { /** * Handler for 'start class' event. * * @param string $name */ protected function startClass($name) { $this->write($this->currentTestClassPrettified . "\n"); } /** * Handler for 'on test' event. * * @param string $name * @param bool $success */ protected function onTest($name, $success = true) { if ($success) { $this->write(' [x] '); } else { $this->write(' [ ] '); } $this->write($name . "\n"); } /** * Handler for 'end class' event. * * @param string $name */ protected function endClass($name) { $this->write("\n"); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Base class for printers of TestDox documentation. * * @since Class available since Release 2.1.0 */ abstract class PHPUnit_Util_TestDox_ResultPrinter extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener { /** * @var PHPUnit_Util_TestDox_NamePrettifier */ protected $prettifier; /** * @var string */ protected $testClass = ''; /** * @var int */ protected $testStatus = false; /** * @var array */ protected $tests = array(); /** * @var int */ protected $successful = 0; /** * @var int */ protected $failed = 0; /** * @var int */ protected $risky = 0; /** * @var int */ protected $skipped = 0; /** * @var int */ protected $incomplete = 0; /** * @var string */ protected $currentTestClassPrettified; /** * @var string */ protected $currentTestMethodPrettified; /** * Constructor. * * @param resource $out */ public function __construct($out = null) { parent::__construct($out); $this->prettifier = new PHPUnit_Util_TestDox_NamePrettifier; $this->startRun(); } /** * Flush buffer and close output. */ public function flush() { $this->doEndClass(); $this->endRun(); parent::flush(); } /** * An error occurred. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { if (!$this->isOfInterest($test)) { return; } $this->testStatus = PHPUnit_Runner_BaseTestRunner::STATUS_ERROR; $this->failed++; } /** * A failure occurred. * * @param PHPUnit_Framework_Test $test * @param PHPUnit_Framework_AssertionFailedError $e * @param float $time */ public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { if (!$this->isOfInterest($test)) { return; } $this->testStatus = PHPUnit_Runner_BaseTestRunner::STATUS_FAILURE; $this->failed++; } /** * Incomplete test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time */ public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) { if (!$this->isOfInterest($test)) { return; } $this->testStatus = PHPUnit_Runner_BaseTestRunner::STATUS_INCOMPLETE; $this->incomplete++; } /** * Risky test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 4.0.0 */ public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) { if (!$this->isOfInterest($test)) { return; } $this->testStatus = PHPUnit_Runner_BaseTestRunner::STATUS_RISKY; $this->risky++; } /** * Skipped test. * * @param PHPUnit_Framework_Test $test * @param Exception $e * @param float $time * * @since Method available since Release 3.0.0 */ public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) { if (!$this->isOfInterest($test)) { return; } $this->testStatus = PHPUnit_Runner_BaseTestRunner::STATUS_SKIPPED; $this->skipped++; } /** * A testsuite started. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function startTestSuite(PHPUnit_Framework_TestSuite $suite) { } /** * A testsuite ended. * * @param PHPUnit_Framework_TestSuite $suite * * @since Method available since Release 2.2.0 */ public function endTestSuite(PHPUnit_Framework_TestSuite $suite) { } /** * A test started. * * @param PHPUnit_Framework_Test $test */ public function startTest(PHPUnit_Framework_Test $test) { if (!$this->isOfInterest($test)) { return; } $class = get_class($test); if ($this->testClass != $class) { if ($this->testClass != '') { $this->doEndClass(); } $this->currentTestClassPrettified = $this->prettifier->prettifyTestClass($class); $this->startClass($class); $this->testClass = $class; $this->tests = array(); } $prettified = false; $annotations = $test->getAnnotations(); if (isset($annotations['method']['testdox'][0])) { $this->currentTestMethodPrettified = $annotations['method']['testdox'][0]; $prettified = true; } if (!$prettified) { $this->currentTestMethodPrettified = $this->prettifier->prettifyTestMethod($test->getName(false)); } $this->testStatus = PHPUnit_Runner_BaseTestRunner::STATUS_PASSED; } /** * A test ended. * * @param PHPUnit_Framework_Test $test * @param float $time */ public function endTest(PHPUnit_Framework_Test $test, $time) { if (!$this->isOfInterest($test)) { return; } if (!isset($this->tests[$this->currentTestMethodPrettified])) { if ($this->testStatus == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED) { $this->tests[$this->currentTestMethodPrettified]['success'] = 1; $this->tests[$this->currentTestMethodPrettified]['failure'] = 0; } else { $this->tests[$this->currentTestMethodPrettified]['success'] = 0; $this->tests[$this->currentTestMethodPrettified]['failure'] = 1; } } else { if ($this->testStatus == PHPUnit_Runner_BaseTestRunner::STATUS_PASSED) { $this->tests[$this->currentTestMethodPrettified]['success']++; } else { $this->tests[$this->currentTestMethodPrettified]['failure']++; } } $this->currentTestClassPrettified = null; $this->currentTestMethodPrettified = null; } /** * @since Method available since Release 2.3.0 */ protected function doEndClass() { foreach ($this->tests as $name => $data) { $this->onTest($name, $data['failure'] == 0); } $this->endClass($this->testClass); } /** * Handler for 'start run' event. */ protected function startRun() { } /** * Handler for 'start class' event. * * @param string $name */ protected function startClass($name) { } /** * Handler for 'on test' event. * * @param string $name * @param bool $success */ protected function onTest($name, $success = true) { } /** * Handler for 'end class' event. * * @param string $name */ protected function endClass($name) { } /** * Handler for 'end run' event. */ protected function endRun() { } private function isOfInterest(PHPUnit_Framework_Test $test) { return $test instanceof PHPUnit_Framework_TestCase && get_class($test) != 'PHPUnit_Framework_Warning'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Iterator for test suites. * * @since Class available since Release 3.1.0 */ class PHPUnit_Util_TestSuiteIterator implements RecursiveIterator { /** * @var int */ protected $position; /** * @var PHPUnit_Framework_Test[] */ protected $tests; /** * @param PHPUnit_Framework_TestSuite $testSuite */ public function __construct(PHPUnit_Framework_TestSuite $testSuite) { $this->tests = $testSuite->tests(); } /** * Rewinds the Iterator to the first element. */ public function rewind() { $this->position = 0; } /** * Checks if there is a current element after calls to rewind() or next(). * * @return bool */ public function valid() { return $this->position < count($this->tests); } /** * Returns the key of the current element. * * @return int */ public function key() { return $this->position; } /** * Returns the current element. * * @return PHPUnit_Framework_Test */ public function current() { return $this->valid() ? $this->tests[$this->position] : null; } /** * Moves forward to next element. */ public function next() { $this->position++; } /** * Returns the sub iterator for the current element. * * @return PHPUnit_Util_TestSuiteIterator */ public function getChildren() { return new self( $this->tests[$this->position] ); } /** * Checks whether the current element has children. * * @return bool */ public function hasChildren() { return $this->tests[$this->position] instanceof PHPUnit_Framework_TestSuite; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Utility class for textual type (and value) representation. * * @since Class available since Release 3.0.0 */ class PHPUnit_Util_Type { public static function isType($type) { return in_array( $type, array( 'numeric', 'integer', 'int', 'float', 'string', 'boolean', 'bool', 'null', 'array', 'object', 'resource', 'scalar' ) ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * XML helpers. * * @since Class available since Release 3.2.0 */ class PHPUnit_Util_XML { /** * Escapes a string for the use in XML documents * Any Unicode character is allowed, excluding the surrogate blocks, FFFE, * and FFFF (not even as character reference). * See http://www.w3.org/TR/xml/#charsets * * @param string $string * * @return string * * @since Method available since Release 3.4.6 */ public static function prepareString($string) { return preg_replace( '/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]/', '', htmlspecialchars( PHPUnit_Util_String::convertToUtf8($string), ENT_QUOTES, 'UTF-8' ) ); } /** * Loads an XML (or HTML) file into a DOMDocument object. * * @param string $filename * @param bool $isHtml * @param bool $xinclude * @param bool $strict * * @return DOMDocument * * @since Method available since Release 3.3.0 */ public static function loadFile($filename, $isHtml = false, $xinclude = false, $strict = false) { $reporting = error_reporting(0); $contents = file_get_contents($filename); error_reporting($reporting); if ($contents === false) { throw new PHPUnit_Framework_Exception( sprintf( 'Could not read "%s".', $filename ) ); } return self::load($contents, $isHtml, $filename, $xinclude, $strict); } /** * Load an $actual document into a DOMDocument. This is called * from the selector assertions. * * If $actual is already a DOMDocument, it is returned with * no changes. Otherwise, $actual is loaded into a new DOMDocument * as either HTML or XML, depending on the value of $isHtml. If $isHtml is * false and $xinclude is true, xinclude is performed on the loaded * DOMDocument. * * Note: prior to PHPUnit 3.3.0, this method loaded a file and * not a string as it currently does. To load a file into a * DOMDocument, use loadFile() instead. * * @param string|DOMDocument $actual * @param bool $isHtml * @param string $filename * @param bool $xinclude * @param bool $strict * * @return DOMDocument * * @since Method available since Release 3.3.0 */ public static function load($actual, $isHtml = false, $filename = '', $xinclude = false, $strict = false) { if ($actual instanceof DOMDocument) { return $actual; } if (!is_string($actual)) { throw new PHPUnit_Framework_Exception('Could not load XML from ' . gettype($actual)); } if ($actual === '') { throw new PHPUnit_Framework_Exception('Could not load XML from empty string'); } // Required for XInclude on Windows. if ($xinclude) { $cwd = getcwd(); @chdir(dirname($filename)); } $document = new DOMDocument; $document->preserveWhiteSpace = false; $internal = libxml_use_internal_errors(true); $message = ''; $reporting = error_reporting(0); if ('' !== $filename) { // Necessary for xinclude $document->documentURI = $filename; } if ($isHtml) { $loaded = $document->loadHTML($actual); } else { $loaded = $document->loadXML($actual); } if (!$isHtml && $xinclude) { $document->xinclude(); } foreach (libxml_get_errors() as $error) { $message .= "\n" . $error->message; } libxml_use_internal_errors($internal); error_reporting($reporting); if ($xinclude) { @chdir($cwd); } if ($loaded === false || ($strict && $message !== '')) { if ($filename !== '') { throw new PHPUnit_Framework_Exception( sprintf( 'Could not load "%s".%s', $filename, $message != '' ? "\n" . $message : '' ) ); } else { if ($message === '') { $message = 'Could not load XML for unknown reason'; } throw new PHPUnit_Framework_Exception($message); } } return $document; } /** * @param DOMNode $node * * @return string * * @since Method available since Release 3.4.0 */ public static function nodeToText(DOMNode $node) { if ($node->childNodes->length == 1) { return $node->textContent; } $result = ''; foreach ($node->childNodes as $childNode) { $result .= $node->ownerDocument->saveXML($childNode); } return $result; } /** * @param DOMNode $node * * @since Method available since Release 3.3.0 */ public static function removeCharacterDataNodes(DOMNode $node) { if ($node->hasChildNodes()) { for ($i = $node->childNodes->length - 1; $i >= 0; $i--) { if (($child = $node->childNodes->item($i)) instanceof DOMCharacterData) { $node->removeChild($child); } } } } /** * "Convert" a DOMElement object into a PHP variable. * * @param DOMElement $element * * @return mixed * * @since Method available since Release 3.4.0 */ public static function xmlToVariable(DOMElement $element) { $variable = null; switch ($element->tagName) { case 'array': $variable = array(); foreach ($element->getElementsByTagName('element') as $element) { $item = $element->childNodes->item(0); if ($item instanceof DOMText) { $item = $element->childNodes->item(1); } $value = self::xmlToVariable($item); if ($element->hasAttribute('key')) { $variable[(string) $element->getAttribute('key')] = $value; } else { $variable[] = $value; } } break; case 'object': $className = $element->getAttribute('class'); if ($element->hasChildNodes()) { $arguments = $element->childNodes->item(1)->childNodes; $constructorArgs = array(); foreach ($arguments as $argument) { if ($argument instanceof DOMElement) { $constructorArgs[] = self::xmlToVariable($argument); } } $class = new ReflectionClass($className); $variable = $class->newInstanceArgs($constructorArgs); } else { $variable = new $className; } break; case 'boolean': $variable = $element->textContent == 'true' ? true : false; break; case 'integer': case 'double': case 'string': $variable = $element->textContent; settype($variable, $element->tagName); break; } return $variable; } /** * Validate list of keys in the associative array. * * @param array $hash * @param array $validKeys * * @return array * * @throws PHPUnit_Framework_Exception * * @since Method available since Release 3.3.0 */ public static function assertValidKeys(array $hash, array $validKeys) { $valids = array(); // Normalize validation keys so that we can use both indexed and // associative arrays. foreach ($validKeys as $key => $val) { is_int($key) ? $valids[$val] = null : $valids[$key] = $val; } $validKeys = array_keys($valids); // Check for invalid keys. foreach ($hash as $key => $value) { if (!in_array($key, $validKeys)) { $unknown[] = $key; } } if (!empty($unknown)) { throw new PHPUnit_Framework_Exception( 'Unknown key(s): ' . implode(', ', $unknown) ); } // Add default values for any valid keys that are empty. foreach ($valids as $key => $value) { if (!isset($hash[$key])) { $hash[$key] = $value; } } return $hash; } /** * Parse a CSS selector into an associative array suitable for * use with findNodes(). * * @param string $selector * @param mixed $content * * @return array * * @since Method available since Release 3.3.0 */ public static function convertSelectToTag($selector, $content = true) { $selector = trim(preg_replace("/\s+/", ' ', $selector)); // substitute spaces within attribute value while (preg_match('/\[[^\]]+"[^"]+\s[^"]+"\]/', $selector)) { $selector = preg_replace( '/(\[[^\]]+"[^"]+)\s([^"]+"\])/', '$1__SPACE__$2', $selector ); } if (strstr($selector, ' ')) { $elements = explode(' ', $selector); } else { $elements = array($selector); } $previousTag = array(); foreach (array_reverse($elements) as $element) { $element = str_replace('__SPACE__', ' ', $element); // child selector if ($element == '>') { $previousTag = array('child' => $previousTag['descendant']); continue; } // adjacent-sibling selector if ($element == '+') { $previousTag = array('adjacent-sibling' => $previousTag['descendant']); continue; } $tag = array(); // match element tag preg_match("/^([^\.#\[]*)/", $element, $eltMatches); if (!empty($eltMatches[1])) { $tag['tag'] = $eltMatches[1]; } // match attributes (\[[^\]]*\]*), ids (#[^\.#\[]*), // and classes (\.[^\.#\[]*)) preg_match_all( "/(\[[^\]]*\]*|#[^\.#\[]*|\.[^\.#\[]*)/", $element, $matches ); if (!empty($matches[1])) { $classes = array(); $attrs = array(); foreach ($matches[1] as $match) { // id matched if (substr($match, 0, 1) == '#') { $tag['id'] = substr($match, 1); } // class matched elseif (substr($match, 0, 1) == '.') { $classes[] = substr($match, 1); } // attribute matched elseif (substr($match, 0, 1) == '[' && substr($match, -1, 1) == ']') { $attribute = substr($match, 1, strlen($match) - 2); $attribute = str_replace('"', '', $attribute); // match single word if (strstr($attribute, '~=')) { list($key, $value) = explode('~=', $attribute); $value = "regexp:/.*\b$value\b.*/"; } // match substring elseif (strstr($attribute, '*=')) { list($key, $value) = explode('*=', $attribute); $value = "regexp:/.*$value.*/"; } // exact match else { list($key, $value) = explode('=', $attribute); } $attrs[$key] = $value; } } if (!empty($classes)) { $tag['class'] = implode(' ', $classes); } if (!empty($attrs)) { $tag['attributes'] = $attrs; } } // tag content if (is_string($content)) { $tag['content'] = $content; } // determine previous child/descendants if (!empty($previousTag['descendant'])) { $tag['descendant'] = $previousTag['descendant']; } elseif (!empty($previousTag['child'])) { $tag['child'] = $previousTag['child']; } elseif (!empty($previousTag['adjacent-sibling'])) { $tag['adjacent-sibling'] = $previousTag['adjacent-sibling']; unset($tag['content']); } $previousTag = array('descendant' => $tag); } return $tag; } /** * Parse an $actual document and return an array of DOMNodes * matching the CSS $selector. If an error occurs, it will * return false. * * To only return nodes containing a certain content, give * the $content to match as a string. Otherwise, setting * $content to true will return all nodes matching $selector. * * The $actual document may be a DOMDocument or a string * containing XML or HTML, identified by $isHtml. * * @param array $selector * @param string $content * @param mixed $actual * @param bool $isHtml * * @return bool|array * * @since Method available since Release 3.3.0 */ public static function cssSelect($selector, $content, $actual, $isHtml = true) { $matcher = self::convertSelectToTag($selector, $content); $dom = self::load($actual, $isHtml); $tags = self::findNodes($dom, $matcher, $isHtml); return $tags; } /** * Parse out the options from the tag using DOM object tree. * * @param DOMDocument $dom * @param array $options * @param bool $isHtml * * @return array * * @since Method available since Release 3.3.0 */ public static function findNodes(DOMDocument $dom, array $options, $isHtml = true) { $valid = array( 'id', 'class', 'tag', 'content', 'attributes', 'parent', 'child', 'ancestor', 'descendant', 'children', 'adjacent-sibling' ); $filtered = array(); $options = self::assertValidKeys($options, $valid); // find the element by id if ($options['id']) { $options['attributes']['id'] = $options['id']; } if ($options['class']) { $options['attributes']['class'] = $options['class']; } $nodes = array(); // find the element by a tag type if ($options['tag']) { if ($isHtml) { $elements = self::getElementsByCaseInsensitiveTagName( $dom, $options['tag'] ); } else { $elements = $dom->getElementsByTagName($options['tag']); } foreach ($elements as $element) { $nodes[] = $element; } if (empty($nodes)) { return false; } } // no tag selected, get them all else { $tags = array( 'a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl', 'dt', 'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'map', 'meta', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'p', 'param', 'pre', 'q', 'samp', 'script', 'select', 'small', 'span', 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'ul', 'var', // HTML5 'article', 'aside', 'audio', 'bdi', 'canvas', 'command', 'datalist', 'details', 'dialog', 'embed', 'figure', 'figcaption', 'footer', 'header', 'hgroup', 'keygen', 'mark', 'meter', 'nav', 'output', 'progress', 'ruby', 'rt', 'rp', 'track', 'section', 'source', 'summary', 'time', 'video', 'wbr' ); foreach ($tags as $tag) { if ($isHtml) { $elements = self::getElementsByCaseInsensitiveTagName( $dom, $tag ); } else { $elements = $dom->getElementsByTagName($tag); } foreach ($elements as $element) { $nodes[] = $element; } } if (empty($nodes)) { return false; } } // filter by attributes if ($options['attributes']) { foreach ($nodes as $node) { $invalid = false; foreach ($options['attributes'] as $name => $value) { // match by regexp if like "regexp:/foo/i" if (preg_match('/^regexp\s*:\s*(.*)/i', $value, $matches)) { if (!preg_match($matches[1], $node->getAttribute($name))) { $invalid = true; } } // class can match only a part elseif ($name == 'class') { // split to individual classes $findClasses = explode( ' ', preg_replace("/\s+/", ' ', $value) ); $allClasses = explode( ' ', preg_replace("/\s+/", ' ', $node->getAttribute($name)) ); // make sure each class given is in the actual node foreach ($findClasses as $findClass) { if (!in_array($findClass, $allClasses)) { $invalid = true; } } } // match by exact string else { if ($node->getAttribute($name) != $value) { $invalid = true; } } } // if every attribute given matched if (!$invalid) { $filtered[] = $node; } } $nodes = $filtered; $filtered = array(); if (empty($nodes)) { return false; } } // filter by content if ($options['content'] !== null) { foreach ($nodes as $node) { $invalid = false; // match by regexp if like "regexp:/foo/i" if (preg_match('/^regexp\s*:\s*(.*)/i', $options['content'], $matches)) { if (!preg_match($matches[1], self::getNodeText($node))) { $invalid = true; } } // match empty string elseif ($options['content'] === '') { if (self::getNodeText($node) !== '') { $invalid = true; } } // match by exact string elseif (strstr(self::getNodeText($node), $options['content']) === false) { $invalid = true; } if (!$invalid) { $filtered[] = $node; } } $nodes = $filtered; $filtered = array(); if (empty($nodes)) { return false; } } // filter by parent node if ($options['parent']) { $parentNodes = self::findNodes($dom, $options['parent'], $isHtml); $parentNode = isset($parentNodes[0]) ? $parentNodes[0] : null; foreach ($nodes as $node) { if ($parentNode !== $node->parentNode) { continue; } $filtered[] = $node; } $nodes = $filtered; $filtered = array(); if (empty($nodes)) { return false; } } // filter by child node if ($options['child']) { $childNodes = self::findNodes($dom, $options['child'], $isHtml); $childNodes = !empty($childNodes) ? $childNodes : array(); foreach ($nodes as $node) { foreach ($node->childNodes as $child) { foreach ($childNodes as $childNode) { if ($childNode === $child) { $filtered[] = $node; } } } } $nodes = $filtered; $filtered = array(); if (empty($nodes)) { return false; } } // filter by adjacent-sibling if ($options['adjacent-sibling']) { $adjacentSiblingNodes = self::findNodes($dom, $options['adjacent-sibling'], $isHtml); $adjacentSiblingNodes = !empty($adjacentSiblingNodes) ? $adjacentSiblingNodes : array(); foreach ($nodes as $node) { $sibling = $node; while ($sibling = $sibling->nextSibling) { if ($sibling->nodeType !== XML_ELEMENT_NODE) { continue; } foreach ($adjacentSiblingNodes as $adjacentSiblingNode) { if ($sibling === $adjacentSiblingNode) { $filtered[] = $node; break; } } break; } } $nodes = $filtered; $filtered = array(); if (empty($nodes)) { return false; } } // filter by ancestor if ($options['ancestor']) { $ancestorNodes = self::findNodes($dom, $options['ancestor'], $isHtml); $ancestorNode = isset($ancestorNodes[0]) ? $ancestorNodes[0] : null; foreach ($nodes as $node) { $parent = $node->parentNode; while ($parent && $parent->nodeType != XML_HTML_DOCUMENT_NODE) { if ($parent === $ancestorNode) { $filtered[] = $node; } $parent = $parent->parentNode; } } $nodes = $filtered; $filtered = array(); if (empty($nodes)) { return false; } } // filter by descendant if ($options['descendant']) { $descendantNodes = self::findNodes($dom, $options['descendant'], $isHtml); $descendantNodes = !empty($descendantNodes) ? $descendantNodes : array(); foreach ($nodes as $node) { foreach (self::getDescendants($node) as $descendant) { foreach ($descendantNodes as $descendantNode) { if ($descendantNode === $descendant) { $filtered[] = $node; } } } } $nodes = $filtered; $filtered = array(); if (empty($nodes)) { return false; } } // filter by children if ($options['children']) { $validChild = array('count', 'greater_than', 'less_than', 'only'); $childOptions = self::assertValidKeys( $options['children'], $validChild ); foreach ($nodes as $node) { $childNodes = $node->childNodes; foreach ($childNodes as $childNode) { if ($childNode->nodeType !== XML_CDATA_SECTION_NODE && $childNode->nodeType !== XML_TEXT_NODE) { $children[] = $childNode; } } // we must have children to pass this filter if (!empty($children)) { // exact count of children if ($childOptions['count'] !== null) { if (count($children) !== $childOptions['count']) { break; } } // range count of children elseif ($childOptions['less_than'] !== null && $childOptions['greater_than'] !== null) { if (count($children) >= $childOptions['less_than'] || count($children) <= $childOptions['greater_than']) { break; } } // less than a given count elseif ($childOptions['less_than'] !== null) { if (count($children) >= $childOptions['less_than']) { break; } } // more than a given count elseif ($childOptions['greater_than'] !== null) { if (count($children) <= $childOptions['greater_than']) { break; } } // match each child against a specific tag if ($childOptions['only']) { $onlyNodes = self::findNodes( $dom, $childOptions['only'], $isHtml ); // try to match each child to one of the 'only' nodes foreach ($children as $child) { $matched = false; foreach ($onlyNodes as $onlyNode) { if ($onlyNode === $child) { $matched = true; } } if (!$matched) { break 2; } } } $filtered[] = $node; } } $nodes = $filtered; if (empty($nodes)) { return; } } // return the first node that matches all criteria return !empty($nodes) ? $nodes : array(); } /** * Recursively get flat array of all descendants of this node. * * @param DOMNode $node * * @return array * * @since Method available since Release 3.3.0 */ protected static function getDescendants(DOMNode $node) { $allChildren = array(); $childNodes = $node->childNodes ? $node->childNodes : array(); foreach ($childNodes as $child) { if ($child->nodeType === XML_CDATA_SECTION_NODE || $child->nodeType === XML_TEXT_NODE) { continue; } $children = self::getDescendants($child); $allChildren = array_merge($allChildren, $children, array($child)); } return isset($allChildren) ? $allChildren : array(); } /** * Gets elements by case insensitive tagname. * * @param DOMDocument $dom * @param string $tag * * @return DOMNodeList * * @since Method available since Release 3.4.0 */ protected static function getElementsByCaseInsensitiveTagName(DOMDocument $dom, $tag) { $elements = $dom->getElementsByTagName(strtolower($tag)); if ($elements->length == 0) { $elements = $dom->getElementsByTagName(strtoupper($tag)); } return $elements; } /** * Get the text value of this node's child text node. * * @param DOMNode $node * * @return string * * @since Method available since Release 3.3.0 */ protected static function getNodeText(DOMNode $node) { if (!$node->childNodes instanceof DOMNodeList) { return ''; } $result = ''; foreach ($node->childNodes as $childNode) { if ($childNode->nodeType === XML_TEXT_NODE || $childNode->nodeType === XML_CDATA_SECTION_NODE) { $result .= trim($childNode->data) . ' '; } else { $result .= self::getNodeText($childNode); } } return str_replace(' ', ' ', $result); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Builder interface for unique identifiers. * * Defines the interface for recording unique identifiers. The identifiers * can be used to define the invocation order of expectations. The expectation * is recorded using id() and then defined in order using * PHPUnit_Framework_MockObject_Builder_Match::after(). * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Builder_Identity { /** * Sets the identification of the expectation to $id. * * @note The identifier is unique per mock object. * @param string $id Unique identifiation of expectation. */ public function id($id); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Builder for mocked or stubbed invocations. * * Provides methods for building expectations without having to resort to * instantiating the various matchers manually. These methods also form a * more natural way of reading the expectation. This class should be together * with the test case PHPUnit_Framework_MockObject_TestCase. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Builder_InvocationMocker implements PHPUnit_Framework_MockObject_Builder_MethodNameMatch { /** * @var PHPUnit_Framework_MockObject_Stub_MatcherCollection */ protected $collection; /** * @var PHPUnit_Framework_MockObject_Matcher */ protected $matcher; /** * @param PHPUnit_Framework_MockObject_Stub_MatcherCollection $collection * @param PHPUnit_Framework_MockObject_Matcher_Invocation $invocationMatcher */ public function __construct(PHPUnit_Framework_MockObject_Stub_MatcherCollection $collection, PHPUnit_Framework_MockObject_Matcher_Invocation $invocationMatcher) { $this->collection = $collection; $this->matcher = new PHPUnit_Framework_MockObject_Matcher( $invocationMatcher ); $this->collection->addMatcher($this->matcher); } /** * @return PHPUnit_Framework_MockObject_Matcher */ public function getMatcher() { return $this->matcher; } /** * @param mixed $id * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function id($id) { $this->collection->registerId($id, $this); return $this; } /** * @param PHPUnit_Framework_MockObject_Stub $stub * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function will(PHPUnit_Framework_MockObject_Stub $stub) { $this->matcher->stub = $stub; return $this; } /** * @param mixed $value * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function willReturn($value) { $stub = new PHPUnit_Framework_MockObject_Stub_Return( $value ); return $this->will($stub); } /** * @param array $valueMap * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function willReturnMap(array $valueMap) { $stub = new PHPUnit_Framework_MockObject_Stub_ReturnValueMap( $valueMap ); return $this->will($stub); } /** * @param mixed $argumentIndex * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function willReturnArgument($argumentIndex) { $stub = new PHPUnit_Framework_MockObject_Stub_ReturnArgument( $argumentIndex ); return $this->will($stub); } /** * @param callable $callback * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function willReturnCallback($callback) { $stub = new PHPUnit_Framework_MockObject_Stub_ReturnCallback( $callback ); return $this->will($stub); } /** * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function willReturnSelf() { $stub = new PHPUnit_Framework_MockObject_Stub_ReturnSelf(); return $this->will($stub); } /** * @param mixed $value, ... * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function willReturnOnConsecutiveCalls() { $args = func_get_args(); $stub = new PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls($args); return $this->will($stub); } /** * @param Exception $exception * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function willThrowException(Exception $exception) { $stub = new PHPUnit_Framework_MockObject_Stub_Exception($exception); return $this->will($stub); } /** * @param mixed $id * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function after($id) { $this->matcher->afterMatchBuilderId = $id; return $this; } /** * Validate that a parameters matcher can be defined, throw exceptions otherwise. * * @throws PHPUnit_Framework_Exception */ private function canDefineParameters() { if ($this->matcher->methodNameMatcher === null) { throw new PHPUnit_Framework_Exception( 'Method name matcher is not defined, cannot define parameter ' . ' matcher without one' ); } if ($this->matcher->parametersMatcher !== null) { throw new PHPUnit_Framework_Exception( 'Parameter matcher is already defined, cannot redefine' ); } } /** * @param mixed $argument, ... * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function with() { $args = func_get_args(); $this->canDefineParameters(); $this->matcher->parametersMatcher = new PHPUnit_Framework_MockObject_Matcher_Parameters($args); return $this; } /** * @param mixed ...$argument * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function withConsecutive() { $args = func_get_args(); $this->canDefineParameters(); $this->matcher->parametersMatcher = new PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters($args); return $this; } /** * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function withAnyParameters() { $this->canDefineParameters(); $this->matcher->parametersMatcher = new PHPUnit_Framework_MockObject_Matcher_AnyParameters; return $this; } /** * @param PHPUnit_Framework_Constraint|string $constraint * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function method($constraint) { if ($this->matcher->methodNameMatcher !== null) { throw new PHPUnit_Framework_Exception( 'Method name matcher is already defined, cannot redefine' ); } $this->matcher->methodNameMatcher = new PHPUnit_Framework_MockObject_Matcher_MethodName($constraint); return $this; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Builder interface for invocation order matches. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Builder_Match extends PHPUnit_Framework_MockObject_Builder_Stub { /** * Defines the expectation which must occur before the current is valid. * * @param string $id The identification of the expectation that should * occur before this one. * @return PHPUnit_Framework_MockObject_Builder_Stub */ public function after($id); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Builder interface for matcher of method names. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Builder_MethodNameMatch extends PHPUnit_Framework_MockObject_Builder_ParametersMatch { /** * Adds a new method name match and returns the parameter match object for * further matching possibilities. * * @param PHPUnit_Framework_Constraint $name * Constraint for matching method, if a string is passed it will use * the PHPUnit_Framework_Constraint_IsEqual. * @return PHPUnit_Framework_MockObject_Builder_ParametersMatch */ public function method($name); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Interface for builders which can register builders with a given identification. * * This interface relates to PHPUnit_Framework_MockObject_Builder_Identity. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Builder_Namespace { /** * Looks up the match builder with identification $id and returns it. * * @param string $id The identifiction of the match builder. * @return PHPUnit_Framework_MockObject_Builder_Match */ public function lookupId($id); /** * Registers the match builder $builder with the identification $id. The * builder can later be looked up using lookupId() to figure out if it * has been invoked. * * @param string $id * The identification of the match builder. * @param PHPUnit_Framework_MockObject_Builder_Match $builder * The builder which is being registered. */ public function registerId($id, PHPUnit_Framework_MockObject_Builder_Match $builder); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Builder interface for parameter matchers. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Builder_ParametersMatch extends PHPUnit_Framework_MockObject_Builder_Match { /** * Sets the parameters to match for, each parameter to this funtion will * be part of match. To perform specific matches or constraints create a * new PHPUnit_Framework_Constraint and use it for the parameter. * If the parameter value is not a constraint it will use the * PHPUnit_Framework_Constraint_IsEqual for the value. * * Some examples: * * // match first parameter with value 2 * $b->with(2); * // match first parameter with value 'smock' and second identical to 42 * $b->with('smock', new PHPUnit_Framework_Constraint_IsEqual(42)); * * * @return PHPUnit_Framework_MockObject_Builder_ParametersMatch */ public function with(); /** * Sets a matcher which allows any kind of parameters. * * Some examples: * * // match any number of parameters * $b->withAnyParamers(); * * * @return PHPUnit_Framework_MockObject_Matcher_AnyParameters */ public function withAnyParameters(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Builder interface for stubs which are actions replacing an invocation. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Builder_Stub extends PHPUnit_Framework_MockObject_Builder_Identity { /** * Stubs the matching method with the stub object $stub. Any invocations of * the matched method will now be handled by the stub instead. * * @param PHPUnit_Framework_MockObject_Stub $stub The stub object. * @return PHPUnit_Framework_MockObject_Builder_Identity */ public function will(PHPUnit_Framework_MockObject_Stub $stub); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.6 */ class PHPUnit_Framework_MockObject_BadMethodCallException extends BadMethodCallException implements PHPUnit_Framework_MockObject_Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Interface for exceptions used by PHPUnit_MockObject. * * @since Interface available since Release 2.0.6 */ interface PHPUnit_Framework_MockObject_Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 2.0.6 */ class PHPUnit_Framework_MockObject_RuntimeException extends RuntimeException implements PHPUnit_Framework_MockObject_Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Doctrine\Instantiator\Instantiator; use Doctrine\Instantiator\Exception\InvalidArgumentException as InstantiatorInvalidArgumentException; use Doctrine\Instantiator\Exception\UnexpectedValueException as InstantiatorUnexpectedValueException; if (!function_exists('trait_exists')) { function trait_exists($traitname, $autoload = true) { return false; } } /** * Mock Object Code Generator * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Generator { /** * @var array */ private static $cache = array(); /** * @var array */ protected $blacklistedMethodNames = array( '__CLASS__' => true, '__DIR__' => true, '__FILE__' => true, '__FUNCTION__' => true, '__LINE__' => true, '__METHOD__' => true, '__NAMESPACE__' => true, '__TRAIT__' => true, '__clone' => true, '__halt_compiler' => true, 'abstract' => true, 'and' => true, 'array' => true, 'as' => true, 'break' => true, 'callable' => true, 'case' => true, 'catch' => true, 'class' => true, 'clone' => true, 'const' => true, 'continue' => true, 'declare' => true, 'default' => true, 'die' => true, 'do' => true, 'echo' => true, 'else' => true, 'elseif' => true, 'empty' => true, 'enddeclare' => true, 'endfor' => true, 'endforeach' => true, 'endif' => true, 'endswitch' => true, 'endwhile' => true, 'eval' => true, 'exit' => true, 'expects' => true, 'extends' => true, 'final' => true, 'for' => true, 'foreach' => true, 'function' => true, 'global' => true, 'goto' => true, 'if' => true, 'implements' => true, 'include' => true, 'include_once' => true, 'instanceof' => true, 'insteadof' => true, 'interface' => true, 'isset' => true, 'list' => true, 'namespace' => true, 'new' => true, 'or' => true, 'print' => true, 'private' => true, 'protected' => true, 'public' => true, 'require' => true, 'require_once' => true, 'return' => true, 'static' => true, 'switch' => true, 'throw' => true, 'trait' => true, 'try' => true, 'unset' => true, 'use' => true, 'var' => true, 'while' => true, 'xor' => true ); /** * Returns a mock object for the specified class. * * @param array|string $type * @param array $methods * @param array $arguments * @param string $mockClassName * @param bool $callOriginalConstructor * @param bool $callOriginalClone * @param bool $callAutoload * @param bool $cloneArguments * @param bool $callOriginalMethods * @param object $proxyTarget * @return object * @throws InvalidArgumentException * @throws PHPUnit_Framework_Exception * @throws PHPUnit_Framework_MockObject_RuntimeException * @since Method available since Release 1.0.0 */ public function getMock($type, $methods = array(), array $arguments = array(), $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false, $proxyTarget = null) { if (!is_array($type) && !is_string($type)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'array or string'); } if (!is_string($mockClassName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(4, 'string'); } if (!is_array($methods) && !is_null($methods)) { throw new InvalidArgumentException; } if ($type === 'Traversable' || $type === '\\Traversable') { $type = 'Iterator'; } if (is_array($type)) { $type = array_unique(array_map( function ($type) { if ($type === 'Traversable' || $type === '\\Traversable' || $type === '\\Iterator') { return 'Iterator'; } return $type; }, $type )); } if (null !== $methods) { foreach ($methods as $method) { if (!preg_match('~[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*~', $method)) { throw new PHPUnit_Framework_Exception( sprintf( 'Cannot stub or mock method with invalid name "%s"', $method ) ); } } if ($methods != array_unique($methods)) { throw new PHPUnit_Framework_MockObject_RuntimeException( sprintf( 'Cannot stub or mock using a method list that contains duplicates: "%s"', implode(', ', $methods) ) ); } } if ($mockClassName != '' && class_exists($mockClassName, false)) { $reflect = new ReflectionClass($mockClassName); if (!$reflect->implementsInterface('PHPUnit_Framework_MockObject_MockObject')) { throw new PHPUnit_Framework_MockObject_RuntimeException( sprintf( 'Class "%s" already exists.', $mockClassName ) ); } } $mock = $this->generate( $type, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods ); return $this->getObject( $mock['code'], $mock['mockClassName'], $type, $callOriginalConstructor, $callAutoload, $arguments, $callOriginalMethods, $proxyTarget ); } /** * @param string $code * @param string $className * @param array|string $type * @param bool $callOriginalConstructor * @param bool $callAutoload * @param array $arguments * @param bool $callOriginalMethods * @param object $proxyTarget * @return object */ protected function getObject($code, $className, $type = '', $callOriginalConstructor = false, $callAutoload = false, array $arguments = array(), $callOriginalMethods = false, $proxyTarget = null) { $this->evalClass($code, $className); if ($callOriginalConstructor && is_string($type) && !interface_exists($type, $callAutoload)) { if (count($arguments) == 0) { $object = new $className; } else { $class = new ReflectionClass($className); $object = $class->newInstanceArgs($arguments); } } else { try { $instantiator = new Instantiator; $object = $instantiator->instantiate($className); } catch (InstantiatorUnexpectedValueException $exception) { if ($exception->getPrevious()) { $exception = $exception->getPrevious(); } throw new PHPUnit_Framework_MockObject_RuntimeException( $exception->getMessage() ); } catch (InstantiatorInvalidArgumentException $exception) { throw new PHPUnit_Framework_MockObject_RuntimeException( $exception->getMessage() ); } } if ($callOriginalMethods) { if (!is_object($proxyTarget)) { if (count($arguments) == 0) { $proxyTarget = new $type; } else { $class = new ReflectionClass($type); $proxyTarget = $class->newInstanceArgs($arguments); } } $object->__phpunit_setOriginalObject($proxyTarget); } return $object; } /** * @param string $code * @param string $className */ protected function evalClass($code, $className) { if (!class_exists($className, false)) { eval($code); } } /** * Returns a mock object for the specified abstract class with all abstract * methods of the class mocked. Concrete methods to mock can be specified with * the last parameter * * @param string $originalClassName * @param array $arguments * @param string $mockClassName * @param bool $callOriginalConstructor * @param bool $callOriginalClone * @param bool $callAutoload * @param array $mockedMethods * @param bool $cloneArguments * @return object * @since Method available since Release 1.0.0 * @throws PHPUnit_Framework_MockObject_RuntimeException * @throws PHPUnit_Framework_Exception */ public function getMockForAbstractClass($originalClassName, array $arguments = array(), $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = array(), $cloneArguments = true) { if (!is_string($originalClassName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($mockClassName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(3, 'string'); } if (class_exists($originalClassName, $callAutoload) || interface_exists($originalClassName, $callAutoload)) { $reflector = new ReflectionClass($originalClassName); $methods = $mockedMethods; foreach ($reflector->getMethods() as $method) { if ($method->isAbstract() && !in_array($method->getName(), $methods)) { $methods[] = $method->getName(); } } if (empty($methods)) { $methods = null; } return $this->getMock( $originalClassName, $methods, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $cloneArguments ); } else { throw new PHPUnit_Framework_MockObject_RuntimeException( sprintf('Class "%s" does not exist.', $originalClassName) ); } } /** * Returns a mock object for the specified trait with all abstract methods * of the trait mocked. Concrete methods to mock can be specified with the * `$mockedMethods` parameter. * * @param string $traitName * @param array $arguments * @param string $mockClassName * @param bool $callOriginalConstructor * @param bool $callOriginalClone * @param bool $callAutoload * @param array $mockedMethods * @param bool $cloneArguments * @return object * @since Method available since Release 1.2.3 * @throws PHPUnit_Framework_MockObject_RuntimeException * @throws PHPUnit_Framework_Exception */ public function getMockForTrait($traitName, array $arguments = array(), $mockClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true, $mockedMethods = array(), $cloneArguments = true) { if (!is_string($traitName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($mockClassName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(3, 'string'); } if (!trait_exists($traitName, $callAutoload)) { throw new PHPUnit_Framework_MockObject_RuntimeException( sprintf( 'Trait "%s" does not exist.', $traitName ) ); } $className = $this->generateClassName( $traitName, '', 'Trait_' ); $templateDir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR; $classTemplate = new Text_Template( $templateDir . 'trait_class.tpl' ); $classTemplate->setVar( array( 'prologue' => 'abstract ', 'class_name' => $className['className'], 'trait_name' => $traitName ) ); $this->evalClass( $classTemplate->render(), $className['className'] ); return $this->getMockForAbstractClass($className['className'], $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $mockedMethods, $cloneArguments); } /** * Returns an object for the specified trait. * * @param string $traitName * @param array $arguments * @param string $traitClassName * @param bool $callOriginalConstructor * @param bool $callOriginalClone * @param bool $callAutoload * @return object * @since Method available since Release 1.1.0 * @throws PHPUnit_Framework_MockObject_RuntimeException * @throws PHPUnit_Framework_Exception */ public function getObjectForTrait($traitName, array $arguments = array(), $traitClassName = '', $callOriginalConstructor = true, $callOriginalClone = true, $callAutoload = true) { if (!is_string($traitName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } if (!is_string($traitClassName)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(3, 'string'); } if (!trait_exists($traitName, $callAutoload)) { throw new PHPUnit_Framework_MockObject_RuntimeException( sprintf( 'Trait "%s" does not exist.', $traitName ) ); } $className = $this->generateClassName( $traitName, $traitClassName, 'Trait_' ); $templateDir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR; $classTemplate = new Text_Template( $templateDir . 'trait_class.tpl' ); $classTemplate->setVar( array( 'prologue' => '', 'class_name' => $className['className'], 'trait_name' => $traitName ) ); return $this->getObject( $classTemplate->render(), $className['className'] ); } /** * @param array|string $type * @param array $methods * @param string $mockClassName * @param bool $callOriginalClone * @param bool $callAutoload * @param bool $cloneArguments * @param bool $callOriginalMethods * @return array */ public function generate($type, array $methods = null, $mockClassName = '', $callOriginalClone = true, $callAutoload = true, $cloneArguments = true, $callOriginalMethods = false) { if (is_array($type)) { sort($type); } if ($mockClassName == '') { $key = md5( is_array($type) ? implode('_', $type) : $type . serialize($methods) . serialize($callOriginalClone) . serialize($cloneArguments) . serialize($callOriginalMethods) ); if (isset(self::$cache[$key])) { return self::$cache[$key]; } } $mock = $this->generateMock( $type, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods ); if (isset($key)) { self::$cache[$key] = $mock; } return $mock; } /** * @param string $wsdlFile * @param string $className * @param array $methods * @param array $options * @return string * @throws PHPUnit_Framework_MockObject_RuntimeException */ public function generateClassFromWsdl($wsdlFile, $className, array $methods = array(), array $options = array()) { if (!extension_loaded('soap')) { throw new PHPUnit_Framework_MockObject_RuntimeException( 'The SOAP extension is required to generate a mock object from WSDL.' ); } $options = array_merge($options, array('cache_wsdl' => WSDL_CACHE_NONE)); $client = new SoapClient($wsdlFile, $options); $_methods = array_unique($client->__getFunctions()); unset($client); sort($_methods); $templateDir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR; $methodTemplate = new Text_Template($templateDir . 'wsdl_method.tpl'); $methodsBuffer = ''; foreach ($_methods as $method) { $nameStart = strpos($method, ' ') + 1; $nameEnd = strpos($method, '('); $name = substr($method, $nameStart, $nameEnd - $nameStart); if (empty($methods) || in_array($name, $methods)) { $args = explode( ',', substr( $method, $nameEnd + 1, strpos($method, ')') - $nameEnd - 1 ) ); $numArgs = count($args); for ($i = 0; $i < $numArgs; $i++) { $args[$i] = substr($args[$i], strpos($args[$i], '$')); } $methodTemplate->setVar( array( 'method_name' => $name, 'arguments' => implode(', ', $args) ) ); $methodsBuffer .= $methodTemplate->render(); } } $optionsBuffer = 'array('; foreach ($options as $key => $value) { $optionsBuffer .= $key . ' => ' . $value; } $optionsBuffer .= ')'; $classTemplate = new Text_Template($templateDir . 'wsdl_class.tpl'); $namespace = ''; if (strpos($className, '\\') !== false) { $parts = explode('\\', $className); $className = array_pop($parts); $namespace = 'namespace ' . implode('\\', $parts) . ';' . "\n\n"; } $classTemplate->setVar( array( 'namespace' => $namespace, 'class_name' => $className, 'wsdl' => $wsdlFile, 'options' => $optionsBuffer, 'methods' => $methodsBuffer ) ); return $classTemplate->render(); } /** * @param array|string $type * @param array|null $methods * @param string $mockClassName * @param bool $callOriginalClone * @param bool $callAutoload * @param bool $cloneArguments * @param bool $callOriginalMethods * @return array * @throws PHPUnit_Framework_Exception */ protected function generateMock($type, $methods, $mockClassName, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods) { $templateDir = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'Generator' . DIRECTORY_SEPARATOR; $classTemplate = new Text_Template( $templateDir . 'mocked_class.tpl' ); $additionalInterfaces = array(); $cloneTemplate = ''; $isClass = false; $isInterface = false; $mockClassName = $this->generateClassName( $type, $mockClassName, 'Mock_' ); if (is_array($type)) { foreach ($type as $_type) { if (!interface_exists($_type, $callAutoload)) { throw new PHPUnit_Framework_Exception( sprintf( 'Interface "%s" does not exist.', $_type ) ); } $additionalInterfaces[] = $_type; foreach ($this->getClassMethods($_type) as $method) { if (in_array($method, $methods)) { throw new PHPUnit_Framework_Exception( sprintf( 'Duplicate method "%s" not allowed.', $method ) ); } $methods[] = $method; } } } if (class_exists($mockClassName['fullClassName'], $callAutoload)) { $isClass = true; } else { if (interface_exists($mockClassName['fullClassName'], $callAutoload)) { $isInterface = true; } } if (!class_exists($mockClassName['fullClassName'], $callAutoload) && !interface_exists($mockClassName['fullClassName'], $callAutoload)) { $prologue = 'class ' . $mockClassName['originalClassName'] . "\n{\n}\n\n"; if (!empty($mockClassName['namespaceName'])) { $prologue = 'namespace ' . $mockClassName['namespaceName'] . " {\n\n" . $prologue . "}\n\n" . "namespace {\n\n"; $epilogue = "\n\n}"; } $cloneTemplate = new Text_Template( $templateDir . 'mocked_clone.tpl' ); } else { $class = new ReflectionClass($mockClassName['fullClassName']); if ($class->isFinal()) { throw new PHPUnit_Framework_Exception( sprintf( 'Class "%s" is declared "final" and cannot be mocked.', $mockClassName['fullClassName'] ) ); } if ($class->hasMethod('__clone')) { $cloneMethod = $class->getMethod('__clone'); if (!$cloneMethod->isFinal()) { if ($callOriginalClone && !$isInterface) { $cloneTemplate = new Text_Template( $templateDir . 'unmocked_clone.tpl' ); } else { $cloneTemplate = new Text_Template( $templateDir . 'mocked_clone.tpl' ); } } } else { $cloneTemplate = new Text_Template( $templateDir . 'mocked_clone.tpl' ); } } if (is_object($cloneTemplate)) { $cloneTemplate = $cloneTemplate->render(); } if (is_array($methods) && empty($methods) && ($isClass || $isInterface)) { $methods = $this->getClassMethods($mockClassName['fullClassName']); } if (!is_array($methods)) { $methods = array(); } $mockedMethods = ''; if (isset($class)) { // https://github.com/sebastianbergmann/phpunit-mock-objects/issues/103 if ($isInterface && $class->implementsInterface('Traversable') && !$class->implementsInterface('Iterator') && !$class->implementsInterface('IteratorAggregate')) { $additionalInterfaces[] = 'Iterator'; $methods = array_merge($methods, $this->getClassMethods('Iterator')); } foreach ($methods as $methodName) { try { $method = $class->getMethod($methodName); if ($this->canMockMethod($method)) { $mockedMethods .= $this->generateMockedMethodDefinitionFromExisting( $templateDir, $method, $cloneArguments, $callOriginalMethods ); } } catch (ReflectionException $e) { $mockedMethods .= $this->generateMockedMethodDefinition( $templateDir, $mockClassName['fullClassName'], $methodName, $cloneArguments ); } } } else { foreach ($methods as $methodName) { $mockedMethods .= $this->generateMockedMethodDefinition( $templateDir, $mockClassName['fullClassName'], $methodName, $cloneArguments ); } } $method = ''; if (!in_array('method', $methods)) { $methodTemplate = new Text_Template( $templateDir . 'mocked_class_method.tpl' ); $method = $methodTemplate->render(); } $classTemplate->setVar( array( 'prologue' => isset($prologue) ? $prologue : '', 'epilogue' => isset($epilogue) ? $epilogue : '', 'class_declaration' => $this->generateMockClassDeclaration( $mockClassName, $isInterface, $additionalInterfaces ), 'clone' => $cloneTemplate, 'mock_class_name' => $mockClassName['className'], 'mocked_methods' => $mockedMethods, 'method' => $method ) ); return array( 'code' => $classTemplate->render(), 'mockClassName' => $mockClassName['className'] ); } /** * @param array|string $type * @param string $className * @param string $prefix * @return array */ protected function generateClassName($type, $className, $prefix) { if (is_array($type)) { $type = implode('_', $type); } if ($type[0] == '\\') { $type = substr($type, 1); } $classNameParts = explode('\\', $type); if (count($classNameParts) > 1) { $type = array_pop($classNameParts); $namespaceName = implode('\\', $classNameParts); $fullClassName = $namespaceName . '\\' . $type; } else { $namespaceName = ''; $fullClassName = $type; } if ($className == '') { do { $className = $prefix . $type . '_' . substr(md5(microtime()), 0, 8); } while (class_exists($className, false)); } return array( 'className' => $className, 'originalClassName' => $type, 'fullClassName' => $fullClassName, 'namespaceName' => $namespaceName ); } /** * @param array $mockClassName * @param bool $isInterface * @param array $additionalInterfaces * @return array */ protected function generateMockClassDeclaration(array $mockClassName, $isInterface, array $additionalInterfaces = array()) { $buffer = 'class '; $additionalInterfaces[] = 'PHPUnit_Framework_MockObject_MockObject'; $interfaces = implode(', ', $additionalInterfaces); if ($isInterface) { $buffer .= sprintf( '%s implements %s', $mockClassName['className'], $interfaces ); if (!in_array($mockClassName['originalClassName'], $additionalInterfaces)) { $buffer .= ', '; if (!empty($mockClassName['namespaceName'])) { $buffer .= $mockClassName['namespaceName'] . '\\'; } $buffer .= $mockClassName['originalClassName']; } } else { $buffer .= sprintf( '%s extends %s%s implements %s', $mockClassName['className'], !empty($mockClassName['namespaceName']) ? $mockClassName['namespaceName'] . '\\' : '', $mockClassName['originalClassName'], $interfaces ); } return $buffer; } /** * @param string $templateDir * @param ReflectionMethod $method * @param bool $cloneArguments * @param bool $callOriginalMethods * @return string */ protected function generateMockedMethodDefinitionFromExisting($templateDir, ReflectionMethod $method, $cloneArguments, $callOriginalMethods) { if ($method->isPrivate()) { $modifier = 'private'; } elseif ($method->isProtected()) { $modifier = 'protected'; } else { $modifier = 'public'; } if ($method->isStatic()) { $modifier .= ' static'; } if ($method->returnsReference()) { $reference = '&'; } else { $reference = ''; } return $this->generateMockedMethodDefinition( $templateDir, $method->getDeclaringClass()->getName(), $method->getName(), $cloneArguments, $modifier, $this->getMethodParameters($method), $this->getMethodParameters($method, true), $reference, $callOriginalMethods, $method->isStatic() ); } /** * @param string $templateDir * @param string $className * @param string $methodName * @param bool $cloneArguments * @param string $modifier * @param string $arguments_decl * @param string $arguments_call * @param string $reference * @param bool $callOriginalMethods * @param bool $static * @return string */ protected function generateMockedMethodDefinition($templateDir, $className, $methodName, $cloneArguments = true, $modifier = 'public', $arguments_decl = '', $arguments_call = '', $reference = '', $callOriginalMethods = false, $static = false) { if ($static) { $templateFile = 'mocked_static_method.tpl'; } else { $templateFile = sprintf( '%s_method.tpl', $callOriginalMethods ? 'proxied' : 'mocked' ); } $template = new Text_Template($templateDir . $templateFile); $template->setVar( array( 'arguments_decl' => $arguments_decl, 'arguments_call' => $arguments_call, 'arguments_count' => !empty($arguments_call) ? count(explode(',', $arguments_call)) : 0, 'class_name' => $className, 'method_name' => $methodName, 'modifier' => $modifier, 'reference' => $reference, 'clone_arguments' => $cloneArguments ? 'TRUE' : 'FALSE' ) ); return $template->render(); } /** * @param ReflectionMethod $method * @return bool */ protected function canMockMethod(ReflectionMethod $method) { if ($method->isConstructor() || $method->isFinal() || $method->isPrivate() || isset($this->blacklistedMethodNames[$method->getName()])) { return false; } return true; } /** * Returns the parameters of a function or method. * * @param ReflectionMethod $method * @param bool $forCall * @return string * @throws PHPUnit_Framework_MockObject_RuntimeException * @since Method available since Release 2.0.0 */ protected function getMethodParameters(ReflectionMethod $method, $forCall = false) { $parameters = array(); foreach ($method->getParameters() as $i => $parameter) { $name = '$' . $parameter->getName(); /* Note: PHP extensions may use empty names for reference arguments * or "..." for methods taking a variable number of arguments. */ if ($name === '$' || $name === '$...') { $name = '$arg' . $i; } if ($this->isVariadic($parameter)) { if ($forCall) { continue; } else { $name = '...' . $name; } } $default = ''; $reference = ''; $typeDeclaration = ''; if (!$forCall) { if ($this->hasType($parameter)) { $typeDeclaration = (string) $parameter->getType() . ' '; } elseif ($parameter->isArray()) { $typeDeclaration = 'array '; } elseif ((defined('HHVM_VERSION') || version_compare(PHP_VERSION, '5.4.0', '>=')) && $parameter->isCallable()) { $typeDeclaration = 'callable '; } else { try { $class = $parameter->getClass(); } catch (ReflectionException $e) { throw new PHPUnit_Framework_MockObject_RuntimeException( sprintf( 'Cannot mock %s::%s() because a class or ' . 'interface used in the signature is not loaded', $method->getDeclaringClass()->getName(), $method->getName() ), 0, $e ); } if ($class !== null) { $typeDeclaration = $class->getName() . ' '; } } if (!$this->isVariadic($parameter)) { if ($parameter->isDefaultValueAvailable()) { $value = $parameter->getDefaultValue(); $default = ' = ' . var_export($value, true); } elseif ($parameter->isOptional()) { $default = ' = null'; } } } if ($parameter->isPassedByReference()) { $reference = '&'; } $parameters[] = $typeDeclaration . $reference . $name . $default; } return implode(', ', $parameters); } /** * @param ReflectionParameter $parameter * @return bool * @since Method available since Release 2.2.1 */ private function isVariadic(ReflectionParameter $parameter) { return method_exists('ReflectionParameter', 'isVariadic') && $parameter->isVariadic(); } /** * @param ReflectionParameter $parameter * @return bool * @since Method available since Release 2.3.4 */ private function hasType(ReflectionParameter $parameter) { return method_exists('ReflectionParameter', 'hasType') && $parameter->hasType(); } /** * @param string $className * @return array * @since Method available since Release 2.3.2 */ private function getClassMethods($className) { $class = new ReflectionClass($className); $methods = array(); foreach ($class->getMethods() as $method) { if (($method->isPublic() || $method->isAbstract()) && !in_array($method->getName(), $methods)) { $methods[] = $method->getName(); } } return $methods; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Represents a non-static invocation. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Invocation_Object extends PHPUnit_Framework_MockObject_Invocation_Static { /** * @var object */ public $object; /** * @param string $className * @param string $methodname * @param array $parameters * @param object $object * @param object $cloneObjects */ public function __construct($className, $methodName, array $parameters, $object, $cloneObjects = false) { parent::__construct($className, $methodName, $parameters, $cloneObjects); $this->object = $object; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Exporter\Exporter; /** * Represents a static invocation. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Invocation_Static implements PHPUnit_Framework_MockObject_Invocation, PHPUnit_Framework_SelfDescribing { /** * @var array */ protected static $uncloneableExtensions = array( 'mysqli' => true, 'SQLite' => true, 'sqlite3' => true, 'tidy' => true, 'xmlwriter' => true, 'xsl' => true ); /** * @var array */ protected static $uncloneableClasses = array( 'Closure', 'COMPersistHelper', 'IteratorIterator', 'RecursiveIteratorIterator', 'SplFileObject', 'PDORow', 'ZipArchive' ); /** * @var string */ public $className; /** * @var string */ public $methodName; /** * @var array */ public $parameters; /** * @param string $className * @param string $methodname * @param array $parameters * @param bool $cloneObjects */ public function __construct($className, $methodName, array $parameters, $cloneObjects = false) { $this->className = $className; $this->methodName = $methodName; $this->parameters = $parameters; if (!$cloneObjects) { return; } foreach ($this->parameters as $key => $value) { if (is_object($value)) { $this->parameters[$key] = $this->cloneObject($value); } } } /** * @return string */ public function toString() { $exporter = new Exporter; return sprintf( '%s::%s(%s)', $this->className, $this->methodName, implode( ', ', array_map( array($exporter, 'shortenedExport'), $this->parameters ) ) ); } /** * @param object $original * @return object */ protected function cloneObject($original) { $cloneable = null; $object = new ReflectionObject($original); // Check the blacklist before asking PHP reflection to work around // https://bugs.php.net/bug.php?id=53967 if ($object->isInternal() && isset(self::$uncloneableExtensions[$object->getExtensionName()])) { $cloneable = false; } if ($cloneable === null) { foreach (self::$uncloneableClasses as $class) { if ($original instanceof $class) { $cloneable = false; break; } } } if ($cloneable === null && method_exists($object, 'isCloneable')) { $cloneable = $object->isCloneable(); } if ($cloneable === null && $object->hasMethod('__clone')) { $method = $object->getMethod('__clone'); $cloneable = $method->isPublic(); } if ($cloneable === null) { $cloneable = true; } if ($cloneable) { try { return clone $original; } catch (Exception $e) { return $original; } } else { return $original; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Interface for invocations. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Invocation { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Mocker for invocations which are sent from * PHPUnit_Framework_MockObject_MockObject objects. * * Keeps track of all expectations and stubs as well as registering * identifications for builders. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_InvocationMocker implements PHPUnit_Framework_MockObject_Stub_MatcherCollection, PHPUnit_Framework_MockObject_Invokable, PHPUnit_Framework_MockObject_Builder_Namespace { /** * @var PHPUnit_Framework_MockObject_Matcher_Invocation[] */ protected $matchers = array(); /** * @var PHPUnit_Framework_MockObject_Builder_Match[] */ protected $builderMap = array(); /** * @param PHPUnit_Framework_MockObject_Matcher_Invocation $matcher */ public function addMatcher(PHPUnit_Framework_MockObject_Matcher_Invocation $matcher) { $this->matchers[] = $matcher; } /** * @since Method available since Release 1.1.0 */ public function hasMatchers() { foreach ($this->matchers as $matcher) { if ($matcher->hasMatchers()) { return true; } } return false; } /** * @param mixed $id * @return bool|null */ public function lookupId($id) { if (isset($this->builderMap[$id])) { return $this->builderMap[$id]; } return; } /** * @param mixed $id * @param PHPUnit_Framework_MockObject_Builder_Match $builder * @throws PHPUnit_Framework_Exception */ public function registerId($id, PHPUnit_Framework_MockObject_Builder_Match $builder) { if (isset($this->builderMap[$id])) { throw new PHPUnit_Framework_Exception( 'Match builder with id <' . $id . '> is already registered.' ); } $this->builderMap[$id] = $builder; } /** * @param PHPUnit_Framework_MockObject_Matcher_Invocation $matcher * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function expects(PHPUnit_Framework_MockObject_Matcher_Invocation $matcher) { return new PHPUnit_Framework_MockObject_Builder_InvocationMocker( $this, $matcher ); } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @return mixed */ public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation) { $exception = null; $hasReturnValue = false; if (strtolower($invocation->methodName) == '__tostring') { $returnValue = ''; } else { $returnValue = null; } foreach ($this->matchers as $match) { try { if ($match->matches($invocation)) { $value = $match->invoked($invocation); if (!$hasReturnValue) { $returnValue = $value; $hasReturnValue = true; } } } catch (Exception $e) { $exception = $e; } } if ($exception !== null) { throw $exception; } return $returnValue; } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @return bool */ public function matches(PHPUnit_Framework_MockObject_Invocation $invocation) { foreach ($this->matchers as $matcher) { if (!$matcher->matches($invocation)) { return false; } } return true; } /** * @return bool */ public function verify() { foreach ($this->matchers as $matcher) { $matcher->verify(); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Interface for classes which can be invoked. * * The invocation will be taken from a mock object and passed to an object * of this class. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Invokable extends PHPUnit_Framework_MockObject_Verifiable { /** * Invokes the invocation object $invocation so that it can be checked for * expectations or matched against stubs. * * @param PHPUnit_Framework_MockObject_Invocation $invocation * The invocation object passed from mock object. * @return object */ public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation); /** * Checks if the invocation matches. * * @param PHPUnit_Framework_MockObject_Invocation $invocation * The invocation object passed from mock object. * @return bool */ public function matches(PHPUnit_Framework_MockObject_Invocation $invocation); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which checks if a method has been invoked zero or more * times. This matcher will always match. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount extends PHPUnit_Framework_MockObject_Matcher_InvokedRecorder { /** * @return string */ public function toString() { return 'invoked zero or more times'; } /** */ public function verify() { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which allows any parameters to a method. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Matcher_AnyParameters extends PHPUnit_Framework_MockObject_Matcher_StatelessInvocation { /** * @return string */ public function toString() { return 'with any parameters'; } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @return bool */ public function matches(PHPUnit_Framework_MockObject_Invocation $invocation) { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which looks for sets of specific parameters in the invocations. * * Checks the parameters of the incoming invocations, the parameter list is * checked against the defined constraints in $parameters. If the constraint * is met it will return true in matches(). * * It takes a list of match groups and and increases a call index after each invocation. * So the first invocation uses the first group of constraints, the second the next and so on. */ class PHPUnit_Framework_MockObject_Matcher_ConsecutiveParameters extends PHPUnit_Framework_MockObject_Matcher_StatelessInvocation { /** * @var array */ private $_parameterGroups = array(); /** * @var array */ private $_invocations = array(); /** * @param array $parameterGroups */ public function __construct(array $parameterGroups) { foreach ($parameterGroups as $index => $parameters) { foreach ($parameters as $parameter) { if (!($parameter instanceof \PHPUnit_Framework_Constraint)) { $parameter = new \PHPUnit_Framework_Constraint_IsEqual($parameter); } $this->_parameterGroups[$index][] = $parameter; } } } /** * @return string */ public function toString() { $text = 'with consecutive parameters'; return $text; } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @return bool */ public function matches(PHPUnit_Framework_MockObject_Invocation $invocation) { $this->_invocations[] = $invocation; $callIndex = count($this->_invocations) - 1; $this->verifyInvocation($invocation, $callIndex); return false; } public function verify() { foreach ($this->_invocations as $callIndex => $invocation) { $this->verifyInvocation($invocation, $callIndex); } } /** * Verify a single invocation * * @param PHPUnit_Framework_MockObject_Invocation $invocation * @param int $callIndex * @throws PHPUnit_Framework_ExpectationFailedException */ private function verifyInvocation(PHPUnit_Framework_MockObject_Invocation $invocation, $callIndex) { if (isset($this->_parameterGroups[$callIndex])) { $parameters = $this->_parameterGroups[$callIndex]; } else { // no parameter assertion for this call index return; } if ($invocation === null) { throw new PHPUnit_Framework_ExpectationFailedException( 'Mocked method does not exist.' ); } if (count($invocation->parameters) < count($parameters)) { throw new PHPUnit_Framework_ExpectationFailedException( sprintf( 'Parameter count for invocation %s is too low.', $invocation->toString() ) ); } foreach ($parameters as $i => $parameter) { $parameter->evaluate( $invocation->parameters[$i], sprintf( 'Parameter %s for invocation #%d %s does not match expected ' . 'value.', $i, $callIndex, $invocation->toString() ) ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Interface for classes which matches an invocation based on its * method name, argument, order or call count. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Matcher_Invocation extends PHPUnit_Framework_SelfDescribing, PHPUnit_Framework_MockObject_Verifiable { /** * Registers the invocation $invocation in the object as being invoked. * This will only occur after matches() returns true which means the * current invocation is the correct one. * * The matcher can store information from the invocation which can later * be checked in verify(), or it can check the values directly and throw * and exception if an expectation is not met. * * If the matcher is a stub it will also have a return value. * * @param PHPUnit_Framework_MockObject_Invocation $invocation * Object containing information on a mocked or stubbed method which * was invoked. * @return mixed */ public function invoked(PHPUnit_Framework_MockObject_Invocation $invocation); /** * Checks if the invocation $invocation matches the current rules. If it does * the matcher will get the invoked() method called which should check if an * expectation is met. * * @param PHPUnit_Framework_MockObject_Invocation $invocation * Object containing information on a mocked or stubbed method which * was invoked. * @return bool */ public function matches(PHPUnit_Framework_MockObject_Invocation $invocation); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which checks if a method was invoked at a certain index. * * If the expected index number does not match the current invocation index it * will not match which means it skips all method and parameter matching. Only * once the index is reached will the method and parameter start matching and * verifying. * * If the index is never reached it will throw an exception in index. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex implements PHPUnit_Framework_MockObject_Matcher_Invocation { /** * @var int */ protected $sequenceIndex; /** * @var int */ protected $currentIndex = -1; /** * @param int $sequenceIndex */ public function __construct($sequenceIndex) { $this->sequenceIndex = $sequenceIndex; } /** * @return string */ public function toString() { return 'invoked at sequence index ' . $this->sequenceIndex; } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @return bool */ public function matches(PHPUnit_Framework_MockObject_Invocation $invocation) { $this->currentIndex++; return $this->currentIndex == $this->sequenceIndex; } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation */ public function invoked(PHPUnit_Framework_MockObject_Invocation $invocation) { } /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws PHPUnit_Framework_ExpectationFailedException */ public function verify() { if ($this->currentIndex < $this->sequenceIndex) { throw new PHPUnit_Framework_ExpectationFailedException( sprintf( 'The expected invocation at index %s was never reached.', $this->sequenceIndex ) ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which checks if a method has been invoked at least * N times. * * @since Class available since Release 2.2.0 */ class PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastCount extends PHPUnit_Framework_MockObject_Matcher_InvokedRecorder { /** * @var int */ private $requiredInvocations; /** * @param int $requiredInvocations */ public function __construct($requiredInvocations) { $this->requiredInvocations = $requiredInvocations; } /** * @return string */ public function toString() { return 'invoked at least ' . $this->requiredInvocations . ' times'; } /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws PHPUnit_Framework_ExpectationFailedException */ public function verify() { $count = $this->getInvocationCount(); if ($count < $this->requiredInvocations) { throw new PHPUnit_Framework_ExpectationFailedException( 'Expected invocation at least ' . $this->requiredInvocations . ' times but it occured ' . $count . ' time(s).' ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which checks if a method has been invoked at least one * time. * * If the number of invocations is 0 it will throw an exception in verify. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce extends PHPUnit_Framework_MockObject_Matcher_InvokedRecorder { /** * @return string */ public function toString() { return 'invoked at least once'; } /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws PHPUnit_Framework_ExpectationFailedException */ public function verify() { $count = $this->getInvocationCount(); if ($count < 1) { throw new PHPUnit_Framework_ExpectationFailedException( 'Expected invocation at least once but it never occured.' ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which checks if a method has been invoked at least * N times. * * @since Class available since Release 2.2.0 */ class PHPUnit_Framework_MockObject_Matcher_InvokedAtMostCount extends PHPUnit_Framework_MockObject_Matcher_InvokedRecorder { /** * @var int */ private $allowedInvocations; /** * @param int $allowedInvocations */ public function __construct($allowedInvocations) { $this->allowedInvocations = $allowedInvocations; } /** * @return string */ public function toString() { return 'invoked at most ' . $this->allowedInvocations . ' times'; } /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws PHPUnit_Framework_ExpectationFailedException */ public function verify() { $count = $this->getInvocationCount(); if ($count > $this->allowedInvocations) { throw new PHPUnit_Framework_ExpectationFailedException( 'Expected invocation at most ' . $this->allowedInvocations . ' times but it occured ' . $count . ' time(s).' ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which checks if a method has been invoked a certain amount * of times. * If the number of invocations exceeds the value it will immediately throw an * exception, * If the number is less it will later be checked in verify() and also throw an * exception. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Matcher_InvokedCount extends PHPUnit_Framework_MockObject_Matcher_InvokedRecorder { /** * @var int */ protected $expectedCount; /** * @param int $expectedCount */ public function __construct($expectedCount) { $this->expectedCount = $expectedCount; } /** * @return bool */ public function isNever() { return $this->expectedCount == 0; } /** * @return string */ public function toString() { return 'invoked ' . $this->expectedCount . ' time(s)'; } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @throws PHPUnit_Framework_ExpectationFailedException */ public function invoked(PHPUnit_Framework_MockObject_Invocation $invocation) { parent::invoked($invocation); $count = $this->getInvocationCount(); if ($count > $this->expectedCount) { $message = $invocation->toString() . ' '; switch ($this->expectedCount) { case 0: { $message .= 'was not expected to be called.'; } break; case 1: { $message .= 'was not expected to be called more than once.'; } break; default: { $message .= sprintf( 'was not expected to be called more than %d times.', $this->expectedCount ); } } throw new PHPUnit_Framework_ExpectationFailedException($message); } } /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws PHPUnit_Framework_ExpectationFailedException */ public function verify() { $count = $this->getInvocationCount(); if ($count !== $this->expectedCount) { throw new PHPUnit_Framework_ExpectationFailedException( sprintf( 'Method was expected to be called %d times, ' . 'actually called %d times.', $this->expectedCount, $count ) ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Records invocations and provides convenience methods for checking them later * on. * This abstract class can be implemented by matchers which needs to check the * number of times an invocation has occured. * * @since Class available since Release 1.0.0 * @abstract */ abstract class PHPUnit_Framework_MockObject_Matcher_InvokedRecorder implements PHPUnit_Framework_MockObject_Matcher_Invocation { /** * @var PHPUnit_Framework_MockObject_Invocation[] */ protected $invocations = array(); /** * @return int */ public function getInvocationCount() { return count($this->invocations); } /** * @return PHPUnit_Framework_MockObject_Invocation[] */ public function getInvocations() { return $this->invocations; } /** * @return bool */ public function hasBeenInvoked() { return count($this->invocations) > 0; } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation */ public function invoked(PHPUnit_Framework_MockObject_Invocation $invocation) { $this->invocations[] = $invocation; } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @return bool */ public function matches(PHPUnit_Framework_MockObject_Invocation $invocation) { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which looks for a specific method name in the invocations. * * Checks the method name all incoming invocations, the name is checked against * the defined constraint $constraint. If the constraint is met it will return * true in matches(). * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Matcher_MethodName extends PHPUnit_Framework_MockObject_Matcher_StatelessInvocation { /** * @var PHPUnit_Framework_Constraint */ protected $constraint; /** * @param PHPUnit_Framework_Constraint|string * @throws PHPUnit_Framework_Constraint */ public function __construct($constraint) { if (!$constraint instanceof PHPUnit_Framework_Constraint) { if (!is_string($constraint)) { throw PHPUnit_Util_InvalidArgumentHelper::factory(1, 'string'); } $constraint = new PHPUnit_Framework_Constraint_IsEqual( $constraint, 0, 10, false, true ); } $this->constraint = $constraint; } /** * @return string */ public function toString() { return 'method name ' . $this->constraint->toString(); } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @return bool */ public function matches(PHPUnit_Framework_MockObject_Invocation $invocation) { return $this->constraint->evaluate($invocation->methodName, '', true); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which looks for specific parameters in the invocations. * * Checks the parameters of all incoming invocations, the parameter list is * checked against the defined constraints in $parameters. If the constraint * is met it will return true in matches(). * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Matcher_Parameters extends PHPUnit_Framework_MockObject_Matcher_StatelessInvocation { /** * @var PHPUnit_Framework_Constraint[] */ protected $parameters = array(); /** * @var PHPUnit_Framework_MockObject_Invocation */ protected $invocation; /** * @param array $parameters */ public function __construct(array $parameters) { foreach ($parameters as $parameter) { if (!($parameter instanceof PHPUnit_Framework_Constraint)) { $parameter = new PHPUnit_Framework_Constraint_IsEqual( $parameter ); } $this->parameters[] = $parameter; } } /** * @return string */ public function toString() { $text = 'with parameter'; foreach ($this->parameters as $index => $parameter) { if ($index > 0) { $text .= ' and'; } $text .= ' ' . $index . ' ' . $parameter->toString(); } return $text; } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @return bool */ public function matches(PHPUnit_Framework_MockObject_Invocation $invocation) { $this->invocation = $invocation; return $this->verify(); } /** * Checks if the invocation $invocation matches the current rules. If it * does the matcher will get the invoked() method called which should check * if an expectation is met. * * @param PHPUnit_Framework_MockObject_Invocation $invocation * Object containing information on a mocked or stubbed method which * was invoked. * @return bool * @throws PHPUnit_Framework_ExpectationFailedException */ public function verify() { if ($this->invocation === null) { throw new PHPUnit_Framework_ExpectationFailedException( 'Mocked method does not exist.' ); } if (count($this->invocation->parameters) < count($this->parameters)) { $message = 'Parameter count for invocation %s is too low.'; // The user called `->with($this->anything())`, but may have meant // `->withAnyParameters()`. // // @see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/199 if (count($this->parameters) === 1 && get_class($this->parameters[0]) === 'PHPUnit_Framework_Constraint_IsAnything') { $message .= "\nTo allow 0 or more parameters with any value, omit ->with() or use ->withAnyParameters() instead."; } throw new PHPUnit_Framework_ExpectationFailedException( sprintf($message, $this->invocation->toString()) ); } foreach ($this->parameters as $i => $parameter) { $parameter->evaluate( $this->invocation->parameters[$i], sprintf( 'Parameter %s for invocation %s does not match expected ' . 'value.', $i, $this->invocation->toString() ) ); } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Invocation matcher which does not care about previous state from earlier * invocations. * * This abstract class can be implemented by matchers which does not care about * state but only the current run-time value of the invocation itself. * * @since Class available since Release 1.0.0 * @abstract */ abstract class PHPUnit_Framework_MockObject_Matcher_StatelessInvocation implements PHPUnit_Framework_MockObject_Matcher_Invocation { /** * Registers the invocation $invocation in the object as being invoked. * This will only occur after matches() returns true which means the * current invocation is the correct one. * * The matcher can store information from the invocation which can later * be checked in verify(), or it can check the values directly and throw * and exception if an expectation is not met. * * If the matcher is a stub it will also have a return value. * * @param PHPUnit_Framework_MockObject_Invocation $invocation * Object containing information on a mocked or stubbed method which * was invoked. * @return mixed */ public function invoked(PHPUnit_Framework_MockObject_Invocation $invocation) { } /** * Checks if the invocation $invocation matches the current rules. If it does * the matcher will get the invoked() method called which should check if an * expectation is met. * * @param PHPUnit_Framework_MockObject_Invocation $invocation * Object containing information on a mocked or stubbed method which * was invoked. * @return bool */ public function verify() { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Main matcher which defines a full expectation using method, parameter and * invocation matchers. * This matcher encapsulates all the other matchers and allows the builder to * set the specific matchers when the appropriate methods are called (once(), * where() etc.). * * All properties are public so that they can easily be accessed by the builder. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Matcher implements PHPUnit_Framework_MockObject_Matcher_Invocation { /** * @var PHPUnit_Framework_MockObject_Matcher_Invocation */ public $invocationMatcher; /** * @var mixed */ public $afterMatchBuilderId = null; /** * @var bool */ public $afterMatchBuilderIsInvoked = false; /** * @var PHPUnit_Framework_MockObject_Matcher_MethodName */ public $methodNameMatcher = null; /** * @var PHPUnit_Framework_MockObject_Matcher_Parameters */ public $parametersMatcher = null; /** * @var PHPUnit_Framework_MockObject_Stub */ public $stub = null; /** * @param PHPUnit_Framework_MockObject_Matcher_Invocation $invocationMatcher */ public function __construct(PHPUnit_Framework_MockObject_Matcher_Invocation $invocationMatcher) { $this->invocationMatcher = $invocationMatcher; } /** * @return string */ public function toString() { $list = array(); if ($this->invocationMatcher !== null) { $list[] = $this->invocationMatcher->toString(); } if ($this->methodNameMatcher !== null) { $list[] = 'where ' . $this->methodNameMatcher->toString(); } if ($this->parametersMatcher !== null) { $list[] = 'and ' . $this->parametersMatcher->toString(); } if ($this->afterMatchBuilderId !== null) { $list[] = 'after ' . $this->afterMatchBuilderId; } if ($this->stub !== null) { $list[] = 'will ' . $this->stub->toString(); } return implode(' ', $list); } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @return mixed */ public function invoked(PHPUnit_Framework_MockObject_Invocation $invocation) { if ($this->invocationMatcher === null) { throw new PHPUnit_Framework_Exception( 'No invocation matcher is set' ); } if ($this->methodNameMatcher === null) { throw new PHPUnit_Framework_Exception('No method matcher is set'); } if ($this->afterMatchBuilderId !== null) { $builder = $invocation->object ->__phpunit_getInvocationMocker() ->lookupId($this->afterMatchBuilderId); if (!$builder) { throw new PHPUnit_Framework_Exception( sprintf( 'No builder found for match builder identification <%s>', $this->afterMatchBuilderId ) ); } $matcher = $builder->getMatcher(); if ($matcher && $matcher->invocationMatcher->hasBeenInvoked()) { $this->afterMatchBuilderIsInvoked = true; } } $this->invocationMatcher->invoked($invocation); try { if ($this->parametersMatcher !== null && !$this->parametersMatcher->matches($invocation)) { $this->parametersMatcher->verify(); } } catch (PHPUnit_Framework_ExpectationFailedException $e) { throw new PHPUnit_Framework_ExpectationFailedException( sprintf( "Expectation failed for %s when %s\n%s", $this->methodNameMatcher->toString(), $this->invocationMatcher->toString(), $e->getMessage() ), $e->getComparisonFailure() ); } if ($this->stub) { return $this->stub->invoke($invocation); } return; } /** * @param PHPUnit_Framework_MockObject_Invocation $invocation * @return bool */ public function matches(PHPUnit_Framework_MockObject_Invocation $invocation) { if ($this->afterMatchBuilderId !== null) { $builder = $invocation->object ->__phpunit_getInvocationMocker() ->lookupId($this->afterMatchBuilderId); if (!$builder) { throw new PHPUnit_Framework_Exception( sprintf( 'No builder found for match builder identification <%s>', $this->afterMatchBuilderId ) ); } $matcher = $builder->getMatcher(); if (!$matcher) { return false; } if (!$matcher->invocationMatcher->hasBeenInvoked()) { return false; } } if ($this->invocationMatcher === null) { throw new PHPUnit_Framework_Exception( 'No invocation matcher is set' ); } if ($this->methodNameMatcher === null) { throw new PHPUnit_Framework_Exception('No method matcher is set'); } if (!$this->invocationMatcher->matches($invocation)) { return false; } try { if (!$this->methodNameMatcher->matches($invocation)) { return false; } } catch (PHPUnit_Framework_ExpectationFailedException $e) { throw new PHPUnit_Framework_ExpectationFailedException( sprintf( "Expectation failed for %s when %s\n%s", $this->methodNameMatcher->toString(), $this->invocationMatcher->toString(), $e->getMessage() ), $e->getComparisonFailure() ); } return true; } /** * @throws PHPUnit_Framework_Exception * @throws PHPUnit_Framework_ExpectationFailedException */ public function verify() { if ($this->invocationMatcher === null) { throw new PHPUnit_Framework_Exception( 'No invocation matcher is set' ); } if ($this->methodNameMatcher === null) { throw new PHPUnit_Framework_Exception('No method matcher is set'); } try { $this->invocationMatcher->verify(); if ($this->parametersMatcher === null) { $this->parametersMatcher = new PHPUnit_Framework_MockObject_Matcher_AnyParameters; } $invocationIsAny = get_class($this->invocationMatcher) === 'PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount'; $invocationIsNever = get_class($this->invocationMatcher) === 'PHPUnit_Framework_MockObject_Matcher_InvokedCount' && $this->invocationMatcher->isNever(); if (!$invocationIsAny && !$invocationIsNever) { $this->parametersMatcher->verify(); } } catch (PHPUnit_Framework_ExpectationFailedException $e) { throw new PHPUnit_Framework_ExpectationFailedException( sprintf( "Expectation failed for %s when %s.\n%s", $this->methodNameMatcher->toString(), $this->invocationMatcher->toString(), PHPUnit_Framework_TestFailure::exceptionToString($e) ) ); } } /** * @since Method available since Release 1.2.4 */ public function hasMatchers() { if ($this->invocationMatcher !== null && !$this->invocationMatcher instanceof PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount) { return true; } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Implementation of the Builder pattern for Mock objects. * * @since File available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_MockBuilder { /** * @var PHPUnit_Framework_TestCase */ private $testCase; /** * @var string */ private $type; /** * @var array */ private $methods = array(); /** * @var string */ private $mockClassName = ''; /** * @var array */ private $constructorArgs = array(); /** * @var bool */ private $originalConstructor = true; /** * @var bool */ private $originalClone = true; /** * @var bool */ private $autoload = true; /** * @var bool */ private $cloneArguments = false; /** * @var bool */ private $callOriginalMethods = false; /** * @var object */ private $proxyTarget = null; /** * @param PHPUnit_Framework_TestCase $testCase * @param array|string $type */ public function __construct(PHPUnit_Framework_TestCase $testCase, $type) { $this->testCase = $testCase; $this->type = $type; } /** * Creates a mock object using a fluent interface. * * @return PHPUnit_Framework_MockObject_MockObject */ public function getMock() { return $this->testCase->getMock( $this->type, $this->methods, $this->constructorArgs, $this->mockClassName, $this->originalConstructor, $this->originalClone, $this->autoload, $this->cloneArguments, $this->callOriginalMethods, $this->proxyTarget ); } /** * Creates a mock object for an abstract class using a fluent interface. * * @return PHPUnit_Framework_MockObject_MockObject */ public function getMockForAbstractClass() { return $this->testCase->getMockForAbstractClass( $this->type, $this->constructorArgs, $this->mockClassName, $this->originalConstructor, $this->originalClone, $this->autoload, $this->methods, $this->cloneArguments ); } /** * Creates a mock object for a trait using a fluent interface. * * @return PHPUnit_Framework_MockObject_MockObject */ public function getMockForTrait() { return $this->testCase->getMockForTrait( $this->type, $this->constructorArgs, $this->mockClassName, $this->originalConstructor, $this->originalClone, $this->autoload, $this->methods, $this->cloneArguments ); } /** * Specifies the subset of methods to mock. Default is to mock all of them. * * @param array|null $methods * @return PHPUnit_Framework_MockObject_MockBuilder */ public function setMethods($methods) { $this->methods = $methods; return $this; } /** * Specifies the arguments for the constructor. * * @param array $args * @return PHPUnit_Framework_MockObject_MockBuilder */ public function setConstructorArgs(array $args) { $this->constructorArgs = $args; return $this; } /** * Specifies the name for the mock class. * * @param string $name * @return PHPUnit_Framework_MockObject_MockBuilder */ public function setMockClassName($name) { $this->mockClassName = $name; return $this; } /** * Disables the invocation of the original constructor. * * @return PHPUnit_Framework_MockObject_MockBuilder */ public function disableOriginalConstructor() { $this->originalConstructor = false; return $this; } /** * Enables the invocation of the original constructor. * * @return PHPUnit_Framework_MockObject_MockBuilder * @since Method available since Release 1.2.0 */ public function enableOriginalConstructor() { $this->originalConstructor = true; return $this; } /** * Disables the invocation of the original clone constructor. * * @return PHPUnit_Framework_MockObject_MockBuilder */ public function disableOriginalClone() { $this->originalClone = false; return $this; } /** * Enables the invocation of the original clone constructor. * * @return PHPUnit_Framework_MockObject_MockBuilder * @since Method available since Release 1.2.0 */ public function enableOriginalClone() { $this->originalClone = true; return $this; } /** * Disables the use of class autoloading while creating the mock object. * * @return PHPUnit_Framework_MockObject_MockBuilder */ public function disableAutoload() { $this->autoload = false; return $this; } /** * Enables the use of class autoloading while creating the mock object. * * @return PHPUnit_Framework_MockObject_MockBuilder * @since Method available since Release 1.2.0 */ public function enableAutoload() { $this->autoload = true; return $this; } /** * Disables the cloning of arguments passed to mocked methods. * * @return PHPUnit_Framework_MockObject_MockBuilder * @since Method available since Release 1.2.0 */ public function disableArgumentCloning() { $this->cloneArguments = false; return $this; } /** * Enables the cloning of arguments passed to mocked methods. * * @return PHPUnit_Framework_MockObject_MockBuilder * @since Method available since Release 1.2.0 */ public function enableArgumentCloning() { $this->cloneArguments = true; return $this; } /** * Enables the invocation of the original methods. * * @return PHPUnit_Framework_MockObject_MockBuilder * @since Method available since Release 2.0.0 */ public function enableProxyingToOriginalMethods() { $this->callOriginalMethods = true; return $this; } /** * Disables the invocation of the original methods. * * @return PHPUnit_Framework_MockObject_MockBuilder * @since Method available since Release 2.0.0 */ public function disableProxyingToOriginalMethods() { $this->callOriginalMethods = false; $this->proxyTarget = null; return $this; } /** * Sets the proxy target. * * @param object $object * @return PHPUnit_Framework_MockObject_MockBuilder * @since Method available since Release 2.0.0 */ public function setProxyTarget($object) { $this->proxyTarget = $object; return $this; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Interface for all mock objects which are generated by * PHPUnit_Framework_MockObject_MockBuilder. * * @method PHPUnit_Framework_MockObject_Builder_InvocationMocker method($constraint) * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_MockObject /*extends PHPUnit_Framework_MockObject_Verifiable*/ { /** * Registers a new expectation in the mock object and returns the match * object which can be infused with further details. * * @param PHPUnit_Framework_MockObject_Matcher_Invocation $matcher * @return PHPUnit_Framework_MockObject_Builder_InvocationMocker */ public function expects(PHPUnit_Framework_MockObject_Matcher_Invocation $matcher); /** * @return PHPUnit_Framework_MockObject_InvocationMocker * @since Method available since Release 2.0.0 */ public function __phpunit_setOriginalObject($originalObject); /** * @return PHPUnit_Framework_MockObject_InvocationMocker */ public function __phpunit_getInvocationMocker(); /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws PHPUnit_Framework_ExpectationFailedException */ public function __phpunit_verify(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Exporter\Exporter; /** * Stubs a method by returning a user-defined stack of values. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls implements PHPUnit_Framework_MockObject_Stub { protected $stack; protected $value; public function __construct($stack) { $this->stack = $stack; } public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation) { $this->value = array_shift($this->stack); if ($this->value instanceof PHPUnit_Framework_MockObject_Stub) { $this->value = $this->value->invoke($invocation); } return $this->value; } public function toString() { $exporter = new Exporter; return sprintf( 'return user-specified value %s', $exporter->export($this->value) ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Exporter\Exporter; /** * Stubs a method by raising a user-defined exception. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Stub_Exception implements PHPUnit_Framework_MockObject_Stub { protected $exception; public function __construct(Exception $exception) { $this->exception = $exception; } public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation) { throw $this->exception; } public function toString() { $exporter = new Exporter; return sprintf( 'raise user-specified exception %s', $exporter->export($this->exception) ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Stubs a method by returning a user-defined value. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Stub_MatcherCollection { /** * Adds a new matcher to the collection which can be used as an expectation * or a stub. * * @param PHPUnit_Framework_MockObject_Matcher_Invocation $matcher * Matcher for invocations to mock objects. */ public function addMatcher(PHPUnit_Framework_MockObject_Matcher_Invocation $matcher); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use SebastianBergmann\Exporter\Exporter; /** * Stubs a method by returning a user-defined value. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Stub_Return implements PHPUnit_Framework_MockObject_Stub { protected $value; public function __construct($value) { $this->value = $value; } public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation) { return $this->value; } public function toString() { $exporter = new Exporter; return sprintf( 'return user-specified value %s', $exporter->export($this->value) ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Stubs a method by returning an argument that was passed to the mocked method. * * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Stub_ReturnArgument extends PHPUnit_Framework_MockObject_Stub_Return { protected $argumentIndex; public function __construct($argumentIndex) { $this->argumentIndex = $argumentIndex; } public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation) { if (isset($invocation->parameters[$this->argumentIndex])) { return $invocation->parameters[$this->argumentIndex]; } else { return; } } public function toString() { return sprintf('return argument #%d', $this->argumentIndex); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * @since Class available since Release 1.0.0 */ class PHPUnit_Framework_MockObject_Stub_ReturnCallback implements PHPUnit_Framework_MockObject_Stub { protected $callback; public function __construct($callback) { $this->callback = $callback; } public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation) { return call_user_func_array($this->callback, $invocation->parameters); } public function toString() { if (is_array($this->callback)) { if (is_object($this->callback[0])) { $class = get_class($this->callback[0]); $type = '->'; } else { $class = $this->callback[0]; $type = '::'; } return sprintf( 'return result of user defined callback %s%s%s() with the ' . 'passed arguments', $class, $type, $this->callback[1] ); } else { return 'return result of user defined callback ' . $this->callback . ' with the passed arguments'; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Stubs a method by returning the current object. * * @since Class available since Release 1.1.0 */ class PHPUnit_Framework_MockObject_Stub_ReturnSelf implements PHPUnit_Framework_MockObject_Stub { public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation) { if (!$invocation instanceof PHPUnit_Framework_MockObject_Invocation_Object) { throw new PHPUnit_Framework_Exception( 'The current object can only be returned when mocking an ' . 'object, not a static class.' ); } return $invocation->object; } public function toString() { return 'return the current object'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Stubs a method by returning a value from a map. * * @since Class available since Release 1.1.0 */ class PHPUnit_Framework_MockObject_Stub_ReturnValueMap implements PHPUnit_Framework_MockObject_Stub { protected $valueMap; public function __construct(array $valueMap) { $this->valueMap = $valueMap; } public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation) { $parameterCount = count($invocation->parameters); foreach ($this->valueMap as $map) { if (!is_array($map) || $parameterCount != count($map) - 1) { continue; } $return = array_pop($map); if ($invocation->parameters === $map) { return $return; } } return; } public function toString() { return 'return value from a map'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * An object that stubs the process of a normal method for a mock object. * * The stub object will replace the code for the stubbed method and return a * specific value instead of the original value. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Stub extends PHPUnit_Framework_SelfDescribing { /** * Fakes the processing of the invocation $invocation by returning a * specific value. * * @param PHPUnit_Framework_MockObject_Invocation $invocation * The invocation which was mocked and matched by the current method * and argument matchers. * @return mixed */ public function invoke(PHPUnit_Framework_MockObject_Invocation $invocation); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Interface for classes which must verify a given expectation. * * @since Interface available since Release 1.0.0 */ interface PHPUnit_Framework_MockObject_Verifiable { /** * Verifies that the current expectation is valid. If everything is OK the * code should just return, if not it must throw an exception. * * @throws PHPUnit_Framework_ExpectationFailedException */ public function verify(); } log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context * * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context * * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context * * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context * * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context * * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context * * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context * * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } } logger = $logger; } } log(LogLevel::EMERGENCY, $message, $context); } /** * Action must be taken immediately. * * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * * @param string $message * @param array $context * * @return void */ public function alert($message, array $context = array()) { $this->log(LogLevel::ALERT, $message, $context); } /** * Critical conditions. * * Example: Application component unavailable, unexpected exception. * * @param string $message * @param array $context * * @return void */ public function critical($message, array $context = array()) { $this->log(LogLevel::CRITICAL, $message, $context); } /** * Runtime errors that do not require immediate action but should typically * be logged and monitored. * * @param string $message * @param array $context * * @return void */ public function error($message, array $context = array()) { $this->log(LogLevel::ERROR, $message, $context); } /** * Exceptional occurrences that are not errors. * * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * * @param string $message * @param array $context * * @return void */ public function warning($message, array $context = array()) { $this->log(LogLevel::WARNING, $message, $context); } /** * Normal but significant events. * * @param string $message * @param array $context * * @return void */ public function notice($message, array $context = array()) { $this->log(LogLevel::NOTICE, $message, $context); } /** * Interesting events. * * Example: User logs in, SQL logs. * * @param string $message * @param array $context * * @return void */ public function info($message, array $context = array()) { $this->log(LogLevel::INFO, $message, $context); } /** * Detailed debug information. * * @param string $message * @param array $context * * @return void */ public function debug($message, array $context = array()) { $this->log(LogLevel::DEBUG, $message, $context); } /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void */ abstract public function log($level, $message, array $context = array()); } logger) { }` * blocks. */ class NullLogger extends AbstractLogger { /** * Logs with an arbitrary level. * * @param mixed $level * @param string $message * @param array $context * * @return void */ public function log($level, $message, array $context = array()) { // noop } } class = $node; $this->abstractMethods = array(); } elseif ($node instanceof ClassMethod) { if ($node->isAbstract()) { $name = sprintf('%s::%s', $this->class->name, $node->name); $this->abstractMethods[] = $name; if ($node->stmts !== null) { throw new FatalErrorException(sprintf('Abstract function %s cannot contain body', $name)); } } } } /** * @throws RuntimeException if the node is a non-abstract class with abstract methods * * @param Node $node */ public function leaveNode(Node $node) { if ($node instanceof ClassStmt) { $count = count($this->abstractMethods); if ($count > 0 && !$node->isAbstract()) { throw new FatalErrorException(sprintf( 'Class %s contains %d abstract method%s must therefore be declared abstract or implement the remaining methods (%s)', $node->name, $count, ($count === 0) ? '' : 's', implode(', ', $this->abstractMethods) )); } } } } */ class AssignThisVariablePass extends CodeCleanerPass { /** * Validate that the user input does not assign the `$this` variable. * * @throws RuntimeException if the user assign the `$this` variable * * @param Node $node */ public function enterNode(Node $node) { if ($node instanceof Assign && $node->var instanceof Variable && $node->var->name === 'this') { throw new FatalErrorException('Cannot re-assign $this'); } } } inClass = false; } /** * @throws ErrorException if get_class or get_called_class is called without an object from outside a class * * @param Node $node */ public function enterNode(Node $node) { if ($node instanceof ClassStmt || $node instanceof TraitStmt) { $this->inClass = true; } elseif ($node instanceof FuncCall && !$this->inClass) { // We'll give any args at all (besides null) a pass. // Technically we should be checking whether the args are objects, but this will do for now. // // TODO: switch this to actually validate args when we get context-aware code cleaner passes. if (!empty($node->args) && !$this->isNull($node->args[0])) { return; } // We'll ignore name expressions as well (things like `$foo()`) if (!($node->name instanceof Name)) { return; } $name = strtolower($node->name); if (in_array($name, array('get_class', 'get_called_class'))) { $msg = sprintf('%s() called without object from outside a class', $name); throw new ErrorException($msg, 0, E_USER_WARNING, null, $node->getLine()); } } } /** * @param Node $node */ public function leaveNode(Node $node) { if ($node instanceof ClassStmt) { $this->inClass = false; } } private function isNull(Node $node) { return $node->value instanceof ConstFetch && strtolower($node->value->name) === 'null'; } } */ class CallTimePassByReferencePass extends CodeCleanerPass { /** * Validate of use call-time pass-by-reference. * * @throws RuntimeException if the user used call-time pass-by-reference in PHP >= 5.4.0 * * @param Node $node */ public function enterNode(Node $node) { if (version_compare(PHP_VERSION, '5.4', '<')) { return; } if (!$node instanceof FunctionCall && !$node instanceof MethodCall && !$node instanceof StaticCall) { return; } foreach ($node->args as $arg) { if ($arg->byRef) { throw new FatalErrorException('Call-time pass-by-reference has been removed'); } } } } */ class FunctionReturnInWriteContextPass extends CodeCleanerPass { const EXCEPTION_MESSAGE = "Can't use function return value in write context"; private $isPhp55; public function __construct() { $this->isPhp55 = version_compare(PHP_VERSION, '5.5', '>='); } /** * Validate that the functions are used correctly. * * @throws FatalErrorException if a function is passed as an argument reference * @throws FatalErrorException if a function is used as an argument in the isset * @throws FatalErrorException if a function is used as an argument in the empty, only for PHP < 5.5 * @throws FatalErrorException if a value is assigned to a function * * @param Node $node */ public function enterNode(Node $node) { if ($node instanceof ArrayNode || $this->isCallNode($node)) { $items = $node instanceof ArrayNode ? $node->items : $node->args; foreach ($items as $item) { if ($item->byRef && $this->isCallNode($item->value)) { throw new FatalErrorException(self::EXCEPTION_MESSAGE); } } } elseif ($node instanceof IssetNode) { foreach ($node->vars as $var) { if (!$this->isCallNode($var)) { continue; } if ($this->isPhp55) { throw new FatalErrorException('Cannot use isset() on the result of a function call (you can use "null !== func()" instead)'); } else { throw new FatalErrorException(self::EXCEPTION_MESSAGE); } } } elseif ($node instanceof EmptyNode && !$this->isPhp55 && $this->isCallNode($node->expr)) { throw new FatalErrorException(self::EXCEPTION_MESSAGE); } elseif ($node instanceof AssignNode && $this->isCallNode($node->var)) { throw new FatalErrorException(self::EXCEPTION_MESSAGE); } } private function isCallNode(Node $node) { return $node instanceof FunctionCall || $node instanceof MethodCall || $node instanceof StaticCall; } } $last->getLine(), 'endLine' => $last->getLine(), )); } return $nodes; } } */ class InstanceOfPass extends CodeCleanerPass { /** * Validate that the instanceof statement does not receive a scalar value or a non-class constant. * * @throws FatalErrorException if a scalar or a non-class constant is given * * @param Node $node */ public function enterNode(Node $node) { if (!$node instanceof InstanceofStmt) { return; } if (($node->expr instanceof Scalar && !$node->expr instanceof Encapsed) || $node->expr instanceof ConstFetch) { throw new FatalErrorException('instanceof expects an object instance, constant given'); } } } name === '__psysh__') { throw new RuntimeException('Don\'t mess with $__psysh__. Bad things will happen.'); } } } =')) { return; } if (!$node instanceof ExprEmpty) { return; } if (!$node->expr instanceof Variable) { $msg = sprintf('syntax error, unexpected %s', $this->getUnexpectedThing($node->expr)); throw new ParseErrorException($msg, $node->expr->getLine()); } } private function getUnexpectedThing(Node $node) { switch ($node->getType()) { case 'Scalar_String': case 'Scalar_LNumber': case 'Scalar_DNumber': return json_encode($node->value); case 'Expr_ConstFetch': return (string) $node->name; default: return $node->getType(); } } } getAttributes()); } elseif ($node instanceof File) { return new StringNode('', $node->getAttributes()); } } } namespace = array(); $this->currentScope = array(); } /** * TODO: should this be final? Extending classes should be sure to either use * leaveNode or call parent::enterNode() when overloading. * * @param Node $node */ public function enterNode(Node $node) { if ($node instanceof NamespaceStmt) { $this->namespace = isset($node->name) ? $node->name->parts : array(); } } /** * Get a fully-qualified name (class, function, interface, etc). * * @param mixed $name * * @return string */ protected function getFullyQualifiedName($name) { if ($name instanceof FullyQualifiedName) { return implode('\\', $name->parts); } elseif ($name instanceof Name) { $name = $name->parts; } elseif (!is_array($name)) { $name = array($name); } return implode('\\', array_merge($this->namespace, $name)); } } cleaner = $cleaner; } /** * If this is a standalone namespace line, remember it for later. * * Otherwise, apply remembered namespaces to the code until a new namespace * is encountered. * * @param array $nodes */ public function beforeTraverse(array $nodes) { $first = reset($nodes); if (count($nodes) === 1 && $first instanceof NamespaceStmt && empty($first->stmts)) { $this->setNamespace($first->name); } else { foreach ($nodes as $key => $node) { if ($node instanceof NamespaceStmt) { $this->setNamespace(null); } elseif ($this->namespace !== null) { $nodes[$key] = new NamespaceStmt($this->namespace, array($node)); } } } return $nodes; } /** * Remember the namespace and (re)set the namespace on the CodeCleaner as * well. * * @param null|Name $namespace */ private function setNamespace($namespace) { $this->namespace = $namespace; $this->cleaner->setNamespace($namespace === null ? null : $namespace->parts); } } name; // if function name is an expression or a variable, give it a pass for now. if ($name instanceof Expr || $name instanceof Variable) { return; } try { $refl = new \ReflectionFunction(implode('\\', $name->parts)); } catch (\ReflectionException $e) { // Well, we gave it a shot! return; } foreach ($refl->getParameters() as $key => $param) { if (array_key_exists($key, $node->args)) { $arg = $node->args[$key]; if ($param->isPassedByReference() && !$this->isPassableByReference($arg)) { throw new FatalErrorException(self::EXCEPTION_MESSAGE); } } } } } private function isPassableByReference(Node $arg) { // FuncCall, MethodCall and StaticCall are all PHP _warnings_ not fatal errors, so we'll let // PHP handle those ones :) return $arg->value instanceof ClassConstFetch || $arg->value instanceof PropertyFetch || $arg->value instanceof Variable || $arg->value instanceof FuncCall || $arg->value instanceof MethodCall || $arg->value instanceof StaticCall; } } */ class StaticConstructorPass extends CodeCleanerPass { private $isPHP533; private $namespace; public function __construct() { $this->isPHP533 = version_compare(PHP_VERSION, '5.3.3', '>='); } public function beforeTraverse(array $nodes) { $this->namespace = array(); } /** * Validate that the old-style constructor function is not static. * * @throws FatalErrorException if the old-style constructor function is static * * @param Node $node */ public function enterNode(Node $node) { if ($node instanceof NamespaceStmt) { $this->namespace = isset($node->name) ? $node->name->parts : array(); } elseif ($node instanceof ClassStmt) { // Bail early if this is PHP 5.3.3 and we have a namespaced class if (!empty($this->namespace) && $this->isPHP533) { return; } $constructor = null; foreach ($node->stmts as $stmt) { if ($stmt instanceof ClassMethod) { // Bail early if we find a new-style constructor if ('__construct' === strtolower($stmt->name)) { return; } // We found a possible old-style constructor // (unless there is also a __construct method) if (strtolower($node->name) === strtolower($stmt->name)) { $constructor = $stmt; } } } if ($constructor && $constructor->isStatic()) { throw new FatalErrorException(sprintf( 'Constructor %s::%s() cannot be static', implode('\\', array_merge($this->namespace, (array) $node->name)), $constructor->name )); } } } } strictTypes; foreach ($nodes as $key => $node) { if ($node instanceof DeclareStmt) { foreach ($node->declares as $declare) { if ($declare->key === 'strict_types') { $value = $declare->value; if (!$value instanceof LNumber || ($value->value !== 0 && $value->value !== 1)) { throw new FatalErrorException('strict_types declaration must have 0 or 1 as its value'); } $this->strictTypes = $value->value === 1; } } } } if ($prependStrictTypes) { $first = reset($nodes); if (!$first instanceof DeclareStmt) { $declare = new DeclareStmt(array(new DeclareDeclare('strict_types', new LNumber(1)))); array_unshift($nodes, $declare); } } return $nodes; } } name) === strtolower($this->lastNamespace)) { $this->aliases = $this->lastAliases; } } } /** * If this statement is a namespace, forget all the aliases we had. * * If it's a use statement, remember the alias for later. Otherwise, apply * remembered aliases to the code. * * @param Node $node */ public function leaveNode(Node $node) { if ($node instanceof UseStmt) { // Store a reference to every "use" statement, because we'll need // them in a bit. foreach ($node->uses as $use) { $this->aliases[strtolower($use->alias)] = $use->name; } return false; } elseif ($node instanceof NamespaceStmt) { // Start fresh, since we're done with this namespace. $this->lastNamespace = $node->name; $this->lastAliases = $this->aliases; $this->aliases = array(); } else { foreach ($node as $name => $subNode) { if ($subNode instanceof Name) { // Implicitly thunk all aliases. if ($replacement = $this->findAlias($subNode)) { $node->$name = $replacement; } } } return $node; } } /** * Find class/namespace aliases. * * @param Name $name * * @return FullyQualifiedName|null */ private function findAlias(Name $name) { $that = strtolower($name); foreach ($this->aliases as $alias => $prefix) { if ($that === $alias) { return new FullyQualifiedName($prefix->toString()); } elseif (substr($that, 0, strlen($alias) + 1) === $alias . '\\') { return new FullyQualifiedName($prefix->toString() . substr($name, strlen($alias))); } } } } checkTraits = function_exists('trait_exists'); } /** * Validate class, interface and trait definitions. * * Validate them upon entering the node, so that we know about their * presence and can validate constant fetches and static calls in class or * trait methods. * * @param Node */ public function enterNode(Node $node) { parent::enterNode($node); if ($node instanceof ClassStmt) { $this->validateClassStatement($node); } elseif ($node instanceof InterfaceStmt) { $this->validateInterfaceStatement($node); } elseif ($node instanceof TraitStmt) { $this->validateTraitStatement($node); } } /** * Validate `new` expressions, class constant fetches, and static calls. * * @throws FatalErrorException if a class, interface or trait is referenced which does not exist * @throws FatalErrorException if a class extends something that is not a class * @throws FatalErrorException if a class implements something that is not an interface * @throws FatalErrorException if an interface extends something that is not an interface * @throws FatalErrorException if a class, interface or trait redefines an existing class, interface or trait name * * @param Node $node */ public function leaveNode(Node $node) { if ($node instanceof NewExpr) { $this->validateNewExpression($node); } elseif ($node instanceof ClassConstFetch) { $this->validateClassConstFetchExpression($node); } elseif ($node instanceof StaticCall) { $this->validateStaticCallExpression($node); } } /** * Validate a class definition statement. * * @param ClassStmt $stmt */ protected function validateClassStatement(ClassStmt $stmt) { $this->ensureCanDefine($stmt); if (isset($stmt->extends)) { $this->ensureClassExists($this->getFullyQualifiedName($stmt->extends), $stmt); } $this->ensureInterfacesExist($stmt->implements, $stmt); } /** * Validate an interface definition statement. * * @param InterfaceStmt $stmt */ protected function validateInterfaceStatement(InterfaceStmt $stmt) { $this->ensureCanDefine($stmt); $this->ensureInterfacesExist($stmt->extends, $stmt); } /** * Validate a trait definition statement. * * @param TraitStmt $stmt */ protected function validateTraitStatement(TraitStmt $stmt) { $this->ensureCanDefine($stmt); } /** * Validate a `new` expression. * * @param NewExpr $stmt */ protected function validateNewExpression(NewExpr $stmt) { // if class name is an expression or an anonymous class, give it a pass for now if (!$stmt->class instanceof Expr && !$stmt->class instanceof ClassStmt) { $this->ensureClassExists($this->getFullyQualifiedName($stmt->class), $stmt); } } /** * Validate a class constant fetch expression's class. * * @param ClassConstFetch $stmt */ protected function validateClassConstFetchExpression(ClassConstFetch $stmt) { // there is no need to check exists for ::class const for php 5.5 or newer if (strtolower($stmt->name) === 'class' && version_compare(PHP_VERSION, '5.5', '>=')) { return; } // if class name is an expression, give it a pass for now if (!$stmt->class instanceof Expr) { $this->ensureClassOrInterfaceExists($this->getFullyQualifiedName($stmt->class), $stmt); } } /** * Validate a class constant fetch expression's class. * * @param StaticCall $stmt */ protected function validateStaticCallExpression(StaticCall $stmt) { // if class name is an expression, give it a pass for now if (!$stmt->class instanceof Expr) { $this->ensureMethodExists($this->getFullyQualifiedName($stmt->class), $stmt->name, $stmt); } } /** * Ensure that no class, interface or trait name collides with a new definition. * * @throws FatalErrorException * * @param Stmt $stmt */ protected function ensureCanDefine(Stmt $stmt) { $name = $this->getFullyQualifiedName($stmt->name); // check for name collisions $errorType = null; if ($this->classExists($name)) { $errorType = self::CLASS_TYPE; } elseif ($this->interfaceExists($name)) { $errorType = self::INTERFACE_TYPE; } elseif ($this->traitExists($name)) { $errorType = self::TRAIT_TYPE; } if ($errorType !== null) { throw $this->createError(sprintf('%s named %s already exists', ucfirst($errorType), $name), $stmt); } // Store creation for the rest of this code snippet so we can find local // issue too $this->currentScope[strtolower($name)] = $this->getScopeType($stmt); } /** * Ensure that a referenced class exists. * * @throws FatalErrorException * * @param string $name * @param Stmt $stmt */ protected function ensureClassExists($name, $stmt) { if (!$this->classExists($name)) { throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt); } } /** * Ensure that a referenced class _or interface_ exists. * * @throws FatalErrorException * * @param string $name * @param Stmt $stmt */ protected function ensureClassOrInterfaceExists($name, $stmt) { if (!$this->classExists($name) && !$this->interfaceExists($name)) { throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt); } } /** * Ensure that a statically called method exists. * * @throws FatalErrorException * * @param string $class * @param string $name * @param Stmt $stmt */ protected function ensureMethodExists($class, $name, $stmt) { $this->ensureClassExists($class, $stmt); // let's pretend all calls to self, parent and static are valid if (in_array(strtolower($class), array('self', 'parent', 'static'))) { return; } // ... and all calls to classes defined right now if ($this->findInScope($class) === self::CLASS_TYPE) { return; } // if method name is an expression, give it a pass for now if ($name instanceof Expr) { return; } if (!method_exists($class, $name) && !method_exists($class, '__callStatic')) { throw $this->createError(sprintf('Call to undefined method %s::%s()', $class, $name), $stmt); } } /** * Ensure that a referenced interface exists. * * @throws FatalErrorException * * @param $interfaces * @param Stmt $stmt */ protected function ensureInterfacesExist($interfaces, $stmt) { foreach ($interfaces as $interface) { /** @var string $name */ $name = $this->getFullyQualifiedName($interface); if (!$this->interfaceExists($name)) { throw $this->createError(sprintf('Interface \'%s\' not found', $name), $stmt); } } } /** * Get a symbol type key for storing in the scope name cache. * * @param Stmt $stmt * * @return string */ protected function getScopeType(Stmt $stmt) { if ($stmt instanceof ClassStmt) { return self::CLASS_TYPE; } elseif ($stmt instanceof InterfaceStmt) { return self::INTERFACE_TYPE; } elseif ($stmt instanceof TraitStmt) { return self::TRAIT_TYPE; } } /** * Check whether a class exists, or has been defined in the current code snippet. * * Gives `self`, `static` and `parent` a free pass. * * @param string $name * * @return bool */ protected function classExists($name) { // Give `self`, `static` and `parent` a pass. This will actually let // some errors through, since we're not checking whether the keyword is // being used in a class scope. if (in_array(strtolower($name), array('self', 'static', 'parent'))) { return true; } return class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE; } /** * Check whether an interface exists, or has been defined in the current code snippet. * * @param string $name * * @return bool */ protected function interfaceExists($name) { return interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE; } /** * Check whether a trait exists, or has been defined in the current code snippet. * * @param string $name * * @return bool */ protected function traitExists($name) { return $this->checkTraits && (trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE); } /** * Find a symbol in the current code snippet scope. * * @param string $name * * @return string|null */ protected function findInScope($name) { $name = strtolower($name); if (isset($this->currentScope[$name])) { return $this->currentScope[$name]; } } /** * Error creation factory. * * @param string $msg * @param Stmt $stmt * * @return FatalErrorException */ protected function createError($msg, $stmt) { return new FatalErrorException($msg, 0, 1, null, $stmt->getLine()); } } name->parts) > 1) { $name = $this->getFullyQualifiedName($node->name); if (!defined($name)) { throw new FatalErrorException(sprintf('Undefined constant %s', $name), 0, 1, null, $node->getLine()); } } elseif ($node instanceof ClassConstFetch) { $this->validateClassConstFetchExpression($node); } } /** * Validate a class constant fetch expression. * * @throws FatalErrorException if a class constant is not defined * * @param ClassConstFetch $stmt */ protected function validateClassConstFetchExpression(ClassConstFetch $stmt) { // give the `class` pseudo-constant a pass if ($stmt->name === 'class') { return; } // if class name is an expression, give it a pass for now if (!$stmt->class instanceof Expr) { $className = $this->getFullyQualifiedName($stmt->class); // if the class doesn't exist, don't throw an exception… it might be // defined in the same line it's used or something stupid like that. if (class_exists($className) || interface_exists($className)) { $constName = sprintf('%s::%s', $className, $stmt->name); if (!defined($constName)) { $constType = class_exists($className) ? 'Class' : 'Interface'; $msg = sprintf('%s constant \'%s\' not found', $constType, $constName); throw new FatalErrorException($msg, 0, 1, null, $stmt->getLine()); } } } } } getFullyQualifiedName($node->name); if (function_exists($name) || isset($this->currentScope[strtolower($name)])) { throw new FatalErrorException(sprintf('Cannot redeclare %s()', $name), 0, 1, null, $node->getLine()); } $this->currentScope[strtolower($name)] = true; } } /** * Validate that function calls will succeed. * * @throws FatalErrorException if a function is redefined * @throws FatalErrorException if the function name is a string (not an expression) and is not defined * * @param Node $node */ public function leaveNode(Node $node) { if ($node instanceof FuncCall) { // if function name is an expression or a variable, give it a pass for now. $name = $node->name; if (!$name instanceof Expr && !$name instanceof Variable) { $shortName = implode('\\', $name->parts); $fullName = $this->getFullyQualifiedName($name); $inScope = isset($this->currentScope[strtolower($fullName)]); if (!$inScope && !function_exists($shortName) && !function_exists($fullName)) { $message = sprintf('Call to undefined function %s()', $name); throw new FatalErrorException($message, 0, 1, null, $node->getLine()); } } } } } createParser(); } $this->parser = $parser; $this->printer = $printer ?: new Printer(); $this->traverser = $traverser ?: new NodeTraverser(); foreach ($this->getDefaultPasses() as $pass) { $this->traverser->addVisitor($pass); } } /** * Get default CodeCleaner passes. * * @return array */ private function getDefaultPasses() { return array( new AbstractClassPass(), new AssignThisVariablePass(), new FunctionReturnInWriteContextPass(), new CallTimePassByReferencePass(), new PassableByReferencePass(), new CalledClassPass(), new InstanceOfPass(), new LeavePsyshAlonePass(), new LegacyEmptyPass(), new ImplicitReturnPass(), new UseStatementPass(), // must run before namespace and validation passes new NamespacePass($this), // must run after the implicit return pass new StrictTypesPass(), new StaticConstructorPass(), new ValidFunctionNamePass(), new ValidClassNamePass(), new ValidConstantPass(), new MagicConstantsPass(), new ExitPass(), ); } /** * Clean the given array of code. * * @throws ParseErrorException if the code is invalid PHP, and cannot be coerced into valid PHP * * @param array $codeLines * @param bool $requireSemicolons * * @return string|false Cleaned PHP code, False if the input is incomplete */ public function clean(array $codeLines, $requireSemicolons = false) { $stmts = $this->parse('traverser->traverse($stmts); return $this->printer->prettyPrint($stmts); } /** * Set the current local namespace. * * @param null|array $namespace (default: null) * * @return null|array */ public function setNamespace(array $namespace = null) { $this->namespace = $namespace; } /** * Get the current local namespace. * * @return null|array */ public function getNamespace() { return $this->namespace; } /** * Lex and parse a block of code. * * @see Parser::parse * * @throws ParseErrorException for parse errors that can't be resolved by * waiting a line to see what comes next * * @param string $code * @param bool $requireSemicolons * * @return array|false A set of statements, or false if incomplete */ protected function parse($code, $requireSemicolons = false) { try { return $this->parser->parse($code); } catch (\PhpParser\Error $e) { if ($this->parseErrorIsUnclosedString($e, $code)) { return false; } if ($this->parseErrorIsUnterminatedComment($e, $code)) { return false; } if (!$this->parseErrorIsEOF($e)) { throw ParseErrorException::fromParseError($e); } if ($requireSemicolons) { return false; } try { // Unexpected EOF, try again with an implicit semicolon return $this->parser->parse($code . ';'); } catch (\PhpParser\Error $e) { return false; } } } private function parseErrorIsEOF(\PhpParser\Error $e) { $msg = $e->getRawMessage(); return ($msg === 'Unexpected token EOF') || (strpos($msg, 'Syntax error, unexpected EOF') !== false); } /** * A special test for unclosed single-quoted strings. * * Unlike (all?) other unclosed statements, single quoted strings have * their own special beautiful snowflake syntax error just for * themselves. * * @param \PhpParser\Error $e * @param string $code * * @return bool */ private function parseErrorIsUnclosedString(\PhpParser\Error $e, $code) { if ($e->getRawMessage() !== 'Syntax error, unexpected T_ENCAPSED_AND_WHITESPACE') { return false; } try { $this->parser->parse($code . "';"); } catch (\Exception $e) { return false; } return true; } private function parseErrorIsUnterminatedComment(\PhpParser\Error $e, $code) { return $e->getRawMessage() === 'Unterminated comment'; } } setName('buffer') ->setAliases(array('buf')) ->setDefinition(array( new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the current buffer.'), )) ->setDescription('Show (or clear) the contents of the code input buffer.') ->setHelp( <<<'HELP' Show the contents of the code buffer for the current multi-line expression. Optionally, clear the buffer by passing the --clear option. HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $buf = $this->getApplication()->getCodeBuffer(); if ($input->getOption('clear')) { $this->getApplication()->resetCodeBuffer(); $output->writeln($this->formatLines($buf, 'urgent'), ShellOutput::NUMBER_LINES); } else { $output->writeln($this->formatLines($buf), ShellOutput::NUMBER_LINES); } } /** * A helper method for wrapping buffer lines in `` and `` formatter strings. * * @param array $lines * @param string $type (default: 'return') * * @return array Formatted strings */ protected function formatLines(array $lines, $type = 'return') { $template = sprintf('<%s>%%s', $type, $type); return array_map(function ($line) use ($template) { return sprintf($template, $line); }, $lines); } } setName('clear') ->setDefinition(array()) ->setDescription('Clear the Psy Shell screen.') ->setHelp( <<<'HELP' Clear the Psy Shell screen. Pro Tip: If your PHP has readline support, you should be able to use ctrl+l too! HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $output->write(sprintf('%c[2J%c[0;0f', 27, 27)); } } Usage:', ' ' . $this->getSynopsis(), '', ); if ($this->getAliases()) { $messages[] = $this->aliasesAsText(); } if ($this->getArguments()) { $messages[] = $this->argumentsAsText(); } if ($this->getOptions()) { $messages[] = $this->optionsAsText(); } if ($help = $this->getProcessedHelp()) { $messages[] = 'Help:'; $messages[] = ' ' . str_replace("\n", "\n ", $help) . "\n"; } return implode("\n", $messages); } /** * {@inheritdoc} */ private function getArguments() { $hidden = $this->getHiddenArguments(); return array_filter($this->getNativeDefinition()->getArguments(), function ($argument) use ($hidden) { return !in_array($argument->getName(), $hidden); }); } /** * These arguments will be excluded from help output. * * @return array */ protected function getHiddenArguments() { return array('command'); } /** * {@inheritdoc} */ private function getOptions() { $hidden = $this->getHiddenOptions(); return array_filter($this->getNativeDefinition()->getOptions(), function ($option) use ($hidden) { return !in_array($option->getName(), $hidden); }); } /** * These options will be excluded from help output. * * @return array */ protected function getHiddenOptions() { return array('verbose'); } /** * Format command aliases as text.. * * @return string */ private function aliasesAsText() { return 'Aliases: ' . implode(', ', $this->getAliases()) . '' . PHP_EOL; } /** * Format command arguments as text. * * @return string */ private function argumentsAsText() { $max = $this->getMaxWidth(); $messages = array(); $arguments = $this->getArguments(); if (!empty($arguments)) { $messages[] = 'Arguments:'; foreach ($arguments as $argument) { if (null !== $argument->getDefault() && (!is_array($argument->getDefault()) || count($argument->getDefault()))) { $default = sprintf(' (default: %s)', $this->formatDefaultValue($argument->getDefault())); } else { $default = ''; } $description = str_replace("\n", "\n" . str_pad('', $max + 2, ' '), $argument->getDescription()); $messages[] = sprintf(" %-${max}s %s%s", $argument->getName(), $description, $default); } $messages[] = ''; } return implode(PHP_EOL, $messages); } /** * Format options as text. * * @return string */ private function optionsAsText() { $max = $this->getMaxWidth(); $messages = array(); $options = $this->getOptions(); if ($options) { $messages[] = 'Options:'; foreach ($options as $option) { if ($option->acceptValue() && null !== $option->getDefault() && (!is_array($option->getDefault()) || count($option->getDefault()))) { $default = sprintf(' (default: %s)', $this->formatDefaultValue($option->getDefault())); } else { $default = ''; } $multiple = $option->isArray() ? ' (multiple values allowed)' : ''; $description = str_replace("\n", "\n" . str_pad('', $max + 2, ' '), $option->getDescription()); $optionMax = $max - strlen($option->getName()) - 2; $messages[] = sprintf( " %s %-${optionMax}s%s%s%s", '--' . $option->getName(), $option->getShortcut() ? sprintf('(-%s) ', $option->getShortcut()) : '', $description, $default, $multiple ); } $messages[] = ''; } return implode(PHP_EOL, $messages); } /** * Calculate the maximum padding width for a set of lines. * * @return int */ private function getMaxWidth() { $max = 0; foreach ($this->getOptions() as $option) { $nameLength = strlen($option->getName()) + 2; if ($option->getShortcut()) { $nameLength += strlen($option->getShortcut()) + 3; } $max = max($max, $nameLength); } foreach ($this->getArguments() as $argument) { $max = max($max, strlen($argument->getName())); } return ++$max; } /** * Format an option default as text. * * @param mixed $default * * @return string */ private function formatDefaultValue($default) { if (is_array($default) && $default === array_values($default)) { return sprintf("array('%s')", implode("', '", $default)); } return str_replace("\n", '', var_export($default, true)); } /** * Get a Table instance. * * Falls back to legacy TableHelper. * * @return Table|TableHelper */ protected function getTable(OutputInterface $output) { if (!class_exists('Symfony\Component\Console\Helper\Table')) { return $this->getTableHelper(); } $style = new TableStyle(); $style ->setVerticalBorderChar(' ') ->setHorizontalBorderChar('') ->setCrossingChar(''); $table = new Table($output); return $table ->setRows(array()) ->setStyle($style); } /** * Legacy fallback for getTable. * * @return TableHelper */ protected function getTableHelper() { $table = $this->getApplication()->getHelperSet()->get('table'); return $table ->setRows(array()) ->setLayout(TableHelper::LAYOUT_BORDERLESS) ->setHorizontalBorderChar('') ->setCrossingChar(''); } } setName('doc') ->setAliases(array('rtfm', 'man')) ->setDefinition(array( new InputArgument('value', InputArgument::REQUIRED, 'Function, class, instance, constant, method or property to document.'), )) ->setDescription('Read the documentation for an object, class, constant, method or property.') ->setHelp( <<>>> doc preg_replace >>> doc Psy\Shell >>> doc Psy\Shell::debug >>> \$s = new Psy\Shell >>> doc \$s->run HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $value = $input->getArgument('value'); if (ReflectionLanguageConstruct::isLanguageConstruct($value)) { $reflector = new ReflectionLanguageConstruct($value); $doc = $this->getManualDocById($value); } else { list($target, $reflector) = $this->getTargetAndReflector($value); $doc = $this->getManualDoc($reflector) ?: DocblockFormatter::format($reflector); } $db = $this->getApplication()->getManualDb(); $output->page(function ($output) use ($reflector, $doc, $db) { $output->writeln(SignatureFormatter::format($reflector)); $output->writeln(''); if (empty($doc) && !$db) { $output->writeln('PHP manual not found'); $output->writeln(' To document core PHP functionality, download the PHP reference manual:'); $output->writeln(' https://github.com/bobthecow/psysh#downloading-the-manual'); } else { $output->writeln($doc); } }); } private function getManualDoc($reflector) { switch (get_class($reflector)) { case 'ReflectionFunction': $id = $reflector->name; break; case 'ReflectionMethod': $id = $reflector->class . '::' . $reflector->name; break; default: return false; } return $this->getManualDocById($id); } private function getManualDocById($id) { if ($db = $this->getApplication()->getManualDb()) { return $db ->query(sprintf('SELECT doc FROM php_manual WHERE id = %s', $db->quote($id))) ->fetchColumn(0); } } } presenter = $presenter; } /** * {@inheritdoc} */ protected function configure() { $this ->setName('dump') ->setDefinition(array( new InputArgument('target', InputArgument::REQUIRED, 'A target object or primitive to dump.', null), new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse', 10), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'), )) ->setDescription('Dump an object or primitive.') ->setHelp( <<<'HELP' Dump an object or primitive. This is like var_dump but way awesomer. e.g. >>> dump $_ >>> dump $someVar HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $depth = $input->getOption('depth'); $target = $this->resolveTarget($input->getArgument('target')); $output->page($this->presenter->present($target, $depth, $input->getOption('all') ? Presenter::VERBOSE : 0)); } /** * Resolve dump target name. * * @throws RuntimeException if target name does not exist in the current scope * * @param string $target * * @return mixed */ protected function resolveTarget($target) { $matches = array(); if (preg_match(self::INSTANCE, $target, $matches)) { return $this->getScopeVariable($matches[1]); } else { throw new RuntimeException('Unknown target: ' . $target); } } } setName('exit') ->setAliases(array('quit', 'q')) ->setDefinition(array()) ->setDescription('End the current session and return to caller.') ->setHelp( <<<'HELP' End the current session and return to caller. e.g. >>> exit HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { throw new BreakException('Goodbye.'); } } setName('help') ->setAliases(array('?')) ->setDefinition(array( new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', null), )) ->setDescription('Show a list of commands. Type `help [foo]` for information about [foo].') ->setHelp('My. How meta.'); } /** * Helper for setting a subcommand to retrieve help for. * * @param Command $command */ public function setCommand($command) { $this->command = $command; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { if ($this->command !== null) { // help for an individual command $output->page($this->command->asText()); $this->command = null; } elseif ($name = $input->getArgument('command_name')) { // help for an individual command $output->page($this->getApplication()->get($name)->asText()); } else { // list available commands $commands = $this->getApplication()->all(); $table = $this->getTable($output); foreach ($commands as $name => $command) { if ($name !== $command->getName()) { continue; } if ($command->getAliases()) { $aliases = sprintf('Aliases: %s', implode(', ', $command->getAliases())); } else { $aliases = ''; } $table->addRow(array( sprintf('%s', $name), $command->getDescription(), $aliases, )); } $output->startPaging(); if ($table instanceof TableHelper) { $table->render($output); } else { $table->render(); } $output->stopPaging(); } } } readline = $readline; } /** * {@inheritdoc} */ protected function configure() { $this ->setName('history') ->setAliases(array('hist')) ->setDefinition(array( new InputOption('show', 's', InputOption::VALUE_REQUIRED, 'Show the given range of lines'), new InputOption('head', 'H', InputOption::VALUE_REQUIRED, 'Display the first N items.'), new InputOption('tail', 'T', InputOption::VALUE_REQUIRED, 'Display the last N items.'), new InputOption('grep', 'G', InputOption::VALUE_REQUIRED, 'Show lines matching the given pattern (string or regex).'), new InputOption('insensitive', 'i', InputOption::VALUE_NONE, 'Case insensitive search (requires --grep).'), new InputOption('invert', 'v', InputOption::VALUE_NONE, 'Inverted search (requires --grep).'), new InputOption('no-numbers', 'N', InputOption::VALUE_NONE, 'Omit line numbers.'), new InputOption('save', '', InputOption::VALUE_REQUIRED, 'Save history to a file.'), new InputOption('replay', '', InputOption::VALUE_NONE, 'Replay'), new InputOption('clear', '', InputOption::VALUE_NONE, 'Clear the history.'), )) ->setDescription('Show the Psy Shell history.') ->setHelp( <<<'HELP' Show, search, save or replay the Psy Shell history. e.g. >>> history --grep /[bB]acon/ >>> history --show 0..10 --replay >>> history --clear >>> history --tail 1000 --save somefile.txt HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $this->validateOnlyOne($input, array('show', 'head', 'tail')); $this->validateOnlyOne($input, array('save', 'replay', 'clear')); $history = $this->getHistorySlice( $input->getOption('show'), $input->getOption('head'), $input->getOption('tail') ); $highlighted = false; $invert = $input->getOption('invert'); $insensitive = $input->getOption('insensitive'); if ($pattern = $input->getOption('grep')) { if (substr($pattern, 0, 1) !== '/' || substr($pattern, -1) !== '/' || strlen($pattern) < 3) { $pattern = '/' . preg_quote($pattern, '/') . '/'; } if ($insensitive) { $pattern .= 'i'; } $this->validateRegex($pattern); $matches = array(); $highlighted = array(); foreach ($history as $i => $line) { if (preg_match($pattern, $line, $matches) xor $invert) { if (!$invert) { $chunks = explode($matches[0], $history[$i]); $chunks = array_map(array(__CLASS__, 'escape'), $chunks); $glue = sprintf('%s', self::escape($matches[0])); $highlighted[$i] = implode($glue, $chunks); } } else { unset($history[$i]); } } } elseif ($invert) { throw new \InvalidArgumentException('Cannot use -v without --grep.'); } elseif ($insensitive) { throw new \InvalidArgumentException('Cannot use -i without --grep.'); } if ($save = $input->getOption('save')) { $output->writeln(sprintf('Saving history in %s...', $save)); file_put_contents($save, implode(PHP_EOL, $history) . PHP_EOL); $output->writeln('History saved.'); } elseif ($input->getOption('replay')) { if (!($input->getOption('show') || $input->getOption('head') || $input->getOption('tail'))) { throw new \InvalidArgumentException('You must limit history via --head, --tail or --show before replaying.'); } $count = count($history); $output->writeln(sprintf('Replaying %d line%s of history', $count, ($count !== 1) ? 's' : '')); $this->getApplication()->addInput($history); } elseif ($input->getOption('clear')) { $this->clearHistory(); $output->writeln('History cleared.'); } else { $type = $input->getOption('no-numbers') ? 0 : ShellOutput::NUMBER_LINES; if (!$highlighted) { $type = $type | ShellOutput::OUTPUT_RAW; } $output->page($highlighted ?: $history, $type); } } /** * Extract a range from a string. * * @param string $range * * @return array [ start, end ] */ private function extractRange($range) { if (preg_match('/^\d+$/', $range)) { return array($range, $range + 1); } $matches = array(); if ($range !== '..' && preg_match('/^(\d*)\.\.(\d*)$/', $range, $matches)) { $start = $matches[1] ? intval($matches[1]) : 0; $end = $matches[2] ? intval($matches[2]) + 1 : PHP_INT_MAX; return array($start, $end); } throw new \InvalidArgumentException('Unexpected range: ' . $range); } /** * Retrieve a slice of the readline history. * * @param string $show * @param string $head * @param string $tail * * @return array A slilce of history */ private function getHistorySlice($show, $head, $tail) { $history = $this->readline->listHistory(); if ($show) { list($start, $end) = $this->extractRange($show); $length = $end - $start; } elseif ($head) { if (!preg_match('/^\d+$/', $head)) { throw new \InvalidArgumentException('Please specify an integer argument for --head.'); } $start = 0; $length = intval($head); } elseif ($tail) { if (!preg_match('/^\d+$/', $tail)) { throw new \InvalidArgumentException('Please specify an integer argument for --tail.'); } $start = count($history) - $tail; $length = intval($tail) + 1; } else { return $history; } return array_slice($history, $start, $length, true); } /** * Validate that $pattern is a valid regular expression. * * @param string $pattern * * @return bool */ private function validateRegex($pattern) { set_error_handler(array('Psy\Exception\ErrorException', 'throwException')); try { preg_match($pattern, ''); } catch (ErrorException $e) { throw new RuntimeException(str_replace('preg_match(): ', 'Invalid regular expression: ', $e->getRawMessage())); } restore_error_handler(); } /** * Validate that only one of the given $options is set. * * @param InputInterface $input * @param array $options */ private function validateOnlyOne(InputInterface $input, array $options) { $count = 0; foreach ($options as $opt) { if ($input->getOption($opt)) { $count++; } } if ($count > 1) { throw new \InvalidArgumentException('Please specify only one of --' . implode(', --', $options)); } } /** * Clear the readline history. */ private function clearHistory() { $this->readline->clearHistory(); } public static function escape($string) { return OutputFormatter::escape($string); } } getOption('constants')) { return; } $constants = $this->prepareConstants($this->getConstants($reflector)); if (empty($constants)) { return; } $ret = array(); $ret[$this->getKindLabel($reflector)] = $constants; return $ret; } /** * Get defined constants for the given class or object Reflector. * * @param \Reflector $reflector * * @return array */ protected function getConstants(\Reflector $reflector) { $constants = array(); foreach ($reflector->getConstants() as $name => $constant) { $constants[$name] = new ReflectionConstant($reflector, $name); } // TODO: this should be natcasesort ksort($constants); return $constants; } /** * Prepare formatted constant array. * * @param array $constants * * @return array */ protected function prepareConstants(array $constants) { // My kingdom for a generator. $ret = array(); foreach ($constants as $name => $constant) { if ($this->showItem($name)) { $ret[$name] = array( 'name' => $name, 'style' => self::IS_CONSTANT, 'value' => $this->presentRef($constant->getValue()), ); } } return $ret; } /** * Get a label for the particular kind of "class" represented. * * @param \ReflectionClass $reflector * * @return string */ protected function getKindLabel(\ReflectionClass $reflector) { if ($reflector->isInterface()) { return 'Interface Constants'; } elseif (method_exists($reflector, 'isTrait') && $reflector->isTrait()) { return 'Trait Constants'; } else { return 'Class Constants'; } } } getOption('classes')) { return; } $classes = $this->prepareClasses(get_declared_classes()); if (empty($classes)) { return; } return array( 'Classes' => $classes, ); } /** * Prepare formatted class array. * * @param array $class * * @return array */ protected function prepareClasses(array $classes) { natcasesort($classes); // My kingdom for a generator. $ret = array(); foreach ($classes as $name) { if ($this->showItem($name)) { $ret[$name] = array( 'name' => $name, 'style' => self::IS_CLASS, 'value' => $this->presentSignature($name), ); } } return $ret; } } getOption('constants')) { return; } $category = $input->getOption('user') ? 'user' : $input->getOption('category'); $label = $category ? ucfirst($category) . ' Constants' : 'Constants'; $constants = $this->prepareConstants($this->getConstants($category)); if (empty($constants)) { return; } $ret = array(); $ret[$label] = $constants; return $ret; } /** * Get defined constants. * * Optionally restrict constants to a given category, e.g. "date". * * @param string $category * * @return array */ protected function getConstants($category = null) { if (!$category) { return get_defined_constants(); } $consts = get_defined_constants(true); return isset($consts[$category]) ? $consts[$category] : array(); } /** * Prepare formatted constant array. * * @param array $constants * * @return array */ protected function prepareConstants(array $constants) { // My kingdom for a generator. $ret = array(); $names = array_keys($constants); natcasesort($names); foreach ($names as $name) { if ($this->showItem($name)) { $ret[$name] = array( 'name' => $name, 'style' => self::IS_CONSTANT, 'value' => $this->presentRef($constants[$name]), ); } } return $ret; } } presenter = $presenter; } /** * Return a list of categorized things with the given input options and target. * * @param InputInterface $input * @param Reflector $reflector * @param mixed $target * * @return array */ public function enumerate(InputInterface $input, \Reflector $reflector = null, $target = null) { $this->setFilter($input); return $this->listItems($input, $reflector, $target); } /** * Enumerate specific items with the given input options and target. * * Implementing classes should return an array of arrays: * * [ * 'Constants' => [ * 'FOO' => [ * 'name' => 'FOO', * 'style' => 'public', * 'value' => '123', * ], * ], * ] * * @param InputInterface $input * @param Reflector $reflector * @param mixed $target * * @return array */ abstract protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null); protected function presentRef($value) { return $this->presenter->presentRef($value); } protected function showItem($name) { return $this->filter === false || (preg_match($this->pattern, $name) xor $this->invertFilter); } private function setFilter(InputInterface $input) { if ($pattern = $input->getOption('grep')) { if (substr($pattern, 0, 1) !== '/' || substr($pattern, -1) !== '/' || strlen($pattern) < 3) { $pattern = '/' . preg_quote($pattern, '/') . '/'; } if ($input->getOption('insensitive')) { $pattern .= 'i'; } $this->validateRegex($pattern); $this->filter = true; $this->pattern = $pattern; $this->invertFilter = $input->getOption('invert'); } else { $this->filter = false; } } /** * Validate that $pattern is a valid regular expression. * * @param string $pattern * * @return bool */ private function validateRegex($pattern) { set_error_handler(array('Psy\Exception\ErrorException', 'throwException')); try { preg_match($pattern, ''); } catch (ErrorException $e) { throw new RuntimeException(str_replace('preg_match(): ', 'Invalid regular expression: ', $e->getRawMessage())); } restore_error_handler(); } protected function presentSignature($target) { // This might get weird if the signature is actually for a reflector. Hrm. if (!$target instanceof \Reflector) { $target = Mirror::get($target); } return SignatureFormatter::format($target); } } getOption('functions')) { return; } if ($input->getOption('user')) { $label = 'User Functions'; $functions = $this->getFunctions('user'); } elseif ($input->getOption('internal')) { $label = 'Internal Functions'; $functions = $this->getFunctions('internal'); } else { $label = 'Functions'; $functions = $this->getFunctions(); } $functions = $this->prepareFunctions($functions); if (empty($functions)) { return; } $ret = array(); $ret[$label] = $functions; return $ret; } /** * Get defined functions. * * Optionally limit functions to "user" or "internal" functions. * * @param null|string $type "user" or "internal" (default: both) * * @return array */ protected function getFunctions($type = null) { $funcs = get_defined_functions(); if ($type) { return $funcs[$type]; } else { return array_merge($funcs['internal'], $funcs['user']); } } /** * Prepare formatted function array. * * @param array $functions * * @return array */ protected function prepareFunctions(array $functions) { natcasesort($functions); // My kingdom for a generator. $ret = array(); foreach ($functions as $name) { if ($this->showItem($name)) { $ret[$name] = array( 'name' => $name, 'style' => self::IS_FUNCTION, 'value' => $this->presentSignature($name), ); } } return $ret; } } getOption('globals')) { return; } $globals = $this->prepareGlobals($this->getGlobals()); if (empty($globals)) { return; } return array( 'Global Variables' => $globals, ); } /** * Get defined global variables. * * @return array */ protected function getGlobals() { global $GLOBALS; $names = array_keys($GLOBALS); natcasesort($names); $ret = array(); foreach ($names as $name) { $ret[$name] = $GLOBALS[$name]; } return $ret; } /** * Prepare formatted global variable array. * * @param array $globals * * @return array */ protected function prepareGlobals($globals) { // My kingdom for a generator. $ret = array(); foreach ($globals as $name => $value) { if ($this->showItem($name)) { $fname = '$' . $name; $ret[$fname] = array( 'name' => $fname, 'style' => self::IS_GLOBAL, 'value' => $this->presentRef($value), ); } } return $ret; } } getOption('interfaces')) { return; } $interfaces = $this->prepareInterfaces(get_declared_interfaces()); if (empty($interfaces)) { return; } return array( 'Interfaces' => $interfaces, ); } /** * Prepare formatted interface array. * * @param array $interfaces * * @return array */ protected function prepareInterfaces(array $interfaces) { natcasesort($interfaces); // My kingdom for a generator. $ret = array(); foreach ($interfaces as $name) { if ($this->showItem($name)) { $ret[$name] = array( 'name' => $name, 'style' => self::IS_CLASS, 'value' => $this->presentSignature($name), ); } } return $ret; } } getOption('methods')) { return; } $showAll = $input->getOption('all'); $methods = $this->prepareMethods($this->getMethods($showAll, $reflector)); if (empty($methods)) { return; } $ret = array(); $ret[$this->getKindLabel($reflector)] = $methods; return $ret; } /** * Get defined methods for the given class or object Reflector. * * @param bool $showAll Include private and protected methods * @param \Reflector $reflector * * @return array */ protected function getMethods($showAll, \Reflector $reflector) { $methods = array(); foreach ($reflector->getMethods() as $name => $method) { if ($showAll || $method->isPublic()) { $methods[$method->getName()] = $method; } } // TODO: this should be natcasesort ksort($methods); return $methods; } /** * Prepare formatted method array. * * @param array $methods * * @return array */ protected function prepareMethods(array $methods) { // My kingdom for a generator. $ret = array(); foreach ($methods as $name => $method) { if ($this->showItem($name)) { $ret[$name] = array( 'name' => $name, 'style' => $this->getVisibilityStyle($method), 'value' => $this->presentSignature($method), ); } } return $ret; } /** * Get a label for the particular kind of "class" represented. * * @param \ReflectionClass $reflector * * @return string */ protected function getKindLabel(\ReflectionClass $reflector) { if ($reflector->isInterface()) { return 'Interface Methods'; } elseif (method_exists($reflector, 'isTrait') && $reflector->isTrait()) { return 'Trait Methods'; } else { return 'Class Methods'; } } /** * Get output style for the given method's visibility. * * @param \ReflectionMethod $method * * @return string */ private function getVisibilityStyle(\ReflectionMethod $method) { if ($method->isPublic()) { return self::IS_PUBLIC; } elseif ($method->isProtected()) { return self::IS_PROTECTED; } else { return self::IS_PRIVATE; } } } getOption('properties')) { return; } $showAll = $input->getOption('all'); $properties = $this->prepareProperties($this->getProperties($showAll, $reflector), $target); if (empty($properties)) { return; } $ret = array(); $ret[$this->getKindLabel($reflector)] = $properties; return $ret; } /** * Get defined properties for the given class or object Reflector. * * @param bool $showAll Include private and protected properties * @param \Reflector $reflector * * @return array */ protected function getProperties($showAll, \Reflector $reflector) { $properties = array(); foreach ($reflector->getProperties() as $property) { if ($showAll || $property->isPublic()) { $properties[$property->getName()] = $property; } } // TODO: this should be natcasesort ksort($properties); return $properties; } /** * Prepare formatted property array. * * @param array $properties * * @return array */ protected function prepareProperties(array $properties, $target = null) { // My kingdom for a generator. $ret = array(); foreach ($properties as $name => $property) { if ($this->showItem($name)) { $fname = '$' . $name; $ret[$fname] = array( 'name' => $fname, 'style' => $this->getVisibilityStyle($property), 'value' => $this->presentValue($property, $target), ); } } return $ret; } /** * Get a label for the particular kind of "class" represented. * * @param \ReflectionClass $reflector * * @return string */ protected function getKindLabel(\ReflectionClass $reflector) { if ($reflector->isInterface()) { return 'Interface Properties'; } elseif (method_exists($reflector, 'isTrait') && $reflector->isTrait()) { return 'Trait Properties'; } else { return 'Class Properties'; } } /** * Get output style for the given property's visibility. * * @param \ReflectionProperty $property * * @return string */ private function getVisibilityStyle(\ReflectionProperty $property) { if ($property->isPublic()) { return self::IS_PUBLIC; } elseif ($property->isProtected()) { return self::IS_PROTECTED; } else { return self::IS_PRIVATE; } } /** * Present the $target's current value for a reflection property. * * @param \ReflectionProperty $property * @param mixed $target * * @return string */ protected function presentValue(\ReflectionProperty $property, $target) { if (!is_object($target)) { // TODO: figure out if there's a way to return defaults when target // is a class/interface/trait rather than an object. return ''; } $property->setAccessible(true); $value = $property->getValue($target); return $this->presentRef($value); } } getOption('traits')) { return; } $traits = $this->prepareTraits(get_declared_traits()); if (empty($traits)) { return; } return array( 'Traits' => $traits, ); } /** * Prepare formatted trait array. * * @param array $traits * * @return array */ protected function prepareTraits(array $traits) { natcasesort($traits); // My kingdom for a generator. $ret = array(); foreach ($traits as $name) { if ($this->showItem($name)) { $ret[$name] = array( 'name' => $name, 'style' => self::IS_CLASS, 'value' => $this->presentSignature($name), ); } } return $ret; } } context = $context; parent::__construct($presenter); } /** * {@inheritdoc} */ protected function listItems(InputInterface $input, \Reflector $reflector = null, $target = null) { // only list variables when no Reflector is present. if ($reflector !== null || $target !== null) { return; } // only list variables if we are specifically asked if (!$input->getOption('vars')) { return; } $showAll = $input->getOption('all'); $variables = $this->prepareVariables($this->getVariables($showAll)); if (empty($variables)) { return; } return array( 'Variables' => $variables, ); } /** * Get scope variables. * * @param bool $showAll Include special variables (e.g. $_) * * @return array */ protected function getVariables($showAll) { $scopeVars = $this->context->getAll(); uksort($scopeVars, function ($a, $b) { if ($a === '_e') { return 1; } elseif ($b === '_e') { return -1; } elseif ($a === '_') { return 1; } elseif ($b === '_') { return -1; } else { // TODO: this should be natcasesort return strcasecmp($a, $b); } }); $ret = array(); foreach ($scopeVars as $name => $val) { if (!$showAll && in_array($name, self::$specialVars)) { continue; } $ret[$name] = $val; } return $ret; } /** * Prepare formatted variable array. * * @param array $variables * * @return array */ protected function prepareVariables(array $variables) { // My kingdom for a generator. $ret = array(); foreach ($variables as $name => $val) { if ($this->showItem($name)) { $fname = '$' . $name; $ret[$fname] = array( 'name' => $fname, 'style' => in_array($name, self::$specialVars) ? self::IS_PRIVATE : self::IS_PUBLIC, 'value' => $this->presentRef($val), // TODO: add types to variable signatures ); } } return $ret; } } presenter = $presenter; } /** * {@inheritdoc} */ protected function configure() { $this ->setName('ls') ->setAliases(array('list', 'dir')) ->setDefinition(array( new InputArgument('target', InputArgument::OPTIONAL, 'A target class or object to list.', null), new InputOption('vars', '', InputOption::VALUE_NONE, 'Display variables.'), new InputOption('constants', 'c', InputOption::VALUE_NONE, 'Display defined constants.'), new InputOption('functions', 'f', InputOption::VALUE_NONE, 'Display defined functions.'), new InputOption('classes', 'k', InputOption::VALUE_NONE, 'Display declared classes.'), new InputOption('interfaces', 'I', InputOption::VALUE_NONE, 'Display declared interfaces.'), new InputOption('traits', 't', InputOption::VALUE_NONE, 'Display declared traits.'), new InputOption('properties', 'p', InputOption::VALUE_NONE, 'Display class or object properties (public properties by default).'), new InputOption('methods', 'm', InputOption::VALUE_NONE, 'Display class or object methods (public methods by default).'), new InputOption('grep', 'G', InputOption::VALUE_REQUIRED, 'Limit to items matching the given pattern (string or regex).'), new InputOption('insensitive', 'i', InputOption::VALUE_NONE, 'Case-insensitive search (requires --grep).'), new InputOption('invert', 'v', InputOption::VALUE_NONE, 'Inverted search (requires --grep).'), new InputOption('globals', 'g', InputOption::VALUE_NONE, 'Include global variables.'), new InputOption('internal', 'n', InputOption::VALUE_NONE, 'Limit to internal functions and classes.'), new InputOption('user', 'u', InputOption::VALUE_NONE, 'Limit to user-defined constants, functions and classes.'), new InputOption('category', 'C', InputOption::VALUE_REQUIRED, 'Limit to constants in a specific category (e.g. "date").'), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Include private and protected methods and properties.'), new InputOption('long', 'l', InputOption::VALUE_NONE, 'List in long format: includes class names and method signatures.'), )) ->setDescription('List local, instance or class variables, methods and constants.') ->setHelp( <<<'HELP' List variables, constants, classes, interfaces, traits, functions, methods, and properties. Called without options, this will return a list of variables currently in scope. If a target object is provided, list properties, constants and methods of that target. If a class, interface or trait name is passed instead, list constants and methods on that class. e.g. >>> ls >>> ls $foo >>> ls -k --grep mongo -i >>> ls -al ReflectionClass >>> ls --constants --category date >>> ls -l --functions --grep /^array_.*/ HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $this->validateInput($input); $this->initEnumerators(); $method = $input->getOption('long') ? 'writeLong' : 'write'; if ($target = $input->getArgument('target')) { list($target, $reflector) = $this->getTargetAndReflector($target, true); } else { $reflector = null; } // TODO: something cleaner than this :-/ if ($input->getOption('long')) { $output->startPaging(); } foreach ($this->enumerators as $enumerator) { $this->$method($output, $enumerator->enumerate($input, $reflector, $target)); } if ($input->getOption('long')) { $output->stopPaging(); } } /** * Initialize Enumerators. */ protected function initEnumerators() { if (!isset($this->enumerators)) { $mgr = $this->presenter; $this->enumerators = array( new ClassConstantEnumerator($mgr), new ClassEnumerator($mgr), new ConstantEnumerator($mgr), new FunctionEnumerator($mgr), new GlobalVariableEnumerator($mgr), new InterfaceEnumerator($mgr), new PropertyEnumerator($mgr), new MethodEnumerator($mgr), new TraitEnumerator($mgr), new VariableEnumerator($mgr, $this->context), ); } } /** * Write the list items to $output. * * @param OutputInterface $output * @param null|array $result List of enumerated items */ protected function write(OutputInterface $output, array $result = null) { if ($result === null) { return; } foreach ($result as $label => $items) { $names = array_map(array($this, 'formatItemName'), $items); $output->writeln(sprintf('%s: %s', $label, implode(', ', $names))); } } /** * Write the list items to $output. * * Items are listed one per line, and include the item signature. * * @param OutputInterface $output * @param null|array $result List of enumerated items */ protected function writeLong(OutputInterface $output, array $result = null) { if ($result === null) { return; } $table = $this->getTable($output); foreach ($result as $label => $items) { $output->writeln(''); $output->writeln(sprintf('%s:', $label)); $table->setRows(array()); foreach ($items as $item) { $table->addRow(array($this->formatItemName($item), $item['value'])); } if ($table instanceof TableHelper) { $table->render($output); } else { $table->render(); } } } /** * Format an item name given its visibility. * * @param array $item * * @return string */ private function formatItemName($item) { return sprintf('<%s>%s', $item['style'], OutputFormatter::escape($item['name']), $item['style']); } /** * Validate that input options make sense, provide defaults when called without options. * * @throws RuntimeException if options are inconsistent * * @param InputInterface $input */ private function validateInput(InputInterface $input) { // grep, invert and insensitive if (!$input->getOption('grep')) { foreach (array('invert', 'insensitive') as $option) { if ($input->getOption($option)) { throw new RuntimeException('--' . $option . ' does not make sense without --grep'); } } } if (!$input->getArgument('target')) { // if no target is passed, there can be no properties or methods foreach (array('properties', 'methods') as $option) { if ($input->getOption($option)) { throw new RuntimeException('--' . $option . ' does not make sense without a specified target.'); } } foreach (array('globals', 'vars', 'constants', 'functions', 'classes', 'interfaces', 'traits') as $option) { if ($input->getOption($option)) { return; } } // default to --vars if no other options are passed $input->setOption('vars', true); } else { // if a target is passed, classes, functions, etc don't make sense foreach (array('vars', 'globals', 'functions', 'classes', 'interfaces', 'traits') as $option) { if ($input->getOption($option)) { throw new RuntimeException('--' . $option . ' does not make sense with a specified target.'); } } foreach (array('constants', 'properties', 'methods') as $option) { if ($input->getOption($option)) { return; } } // default to --constants --properties --methods if no other options are passed $input->setOption('constants', true); $input->setOption('properties', true); $input->setOption('methods', true); } } } parserFactory = new ParserFactory(); $this->parsers = array(); parent::__construct($name); } /** * PresenterAware interface. * * @param Presenter $presenter */ public function setPresenter(Presenter $presenter) { $this->presenter = clone $presenter; $this->presenter->addCasters(array( 'PhpParser\Node' => function (Node $node, array $a) { $a = array( Caster::PREFIX_VIRTUAL . 'type' => $node->getType(), Caster::PREFIX_VIRTUAL . 'attributes' => $node->getAttributes(), ); foreach ($node->getSubNodeNames() as $name) { $a[Caster::PREFIX_VIRTUAL . $name] = $node->$name; } return $a; }, )); } /** * {@inheritdoc} */ protected function configure() { $definition = array( new InputArgument('code', InputArgument::REQUIRED, 'PHP code to parse.'), new InputOption('depth', '', InputOption::VALUE_REQUIRED, 'Depth to parse', 10), ); if ($this->parserFactory->hasKindsSupport()) { $msg = 'One of PhpParser\\ParserFactory constants: ' . implode(', ', ParserFactory::getPossibleKinds()) . " (default is based on current interpreter's version)"; $defaultKind = $this->parserFactory->getDefaultKind(); $definition[] = new InputOption('kind', '', InputOption::VALUE_REQUIRED, $msg, $defaultKind); } $this ->setName('parse') ->setDefinition($definition) ->setDescription('Parse PHP code and show the abstract syntax tree.') ->setHelp( <<<'HELP' Parse PHP code and show the abstract syntax tree. This command is used in the development of PsySH. Given a string of PHP code, it pretty-prints the PHP Parser parse tree. See https://github.com/nikic/PHP-Parser It prolly won't be super useful for most of you, but it's here if you want to play. HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $code = $input->getArgument('code'); if (strpos('getOption('kind'); $depth = $input->getOption('depth'); $nodes = $this->parse($this->getParser($parserKind), $code); $output->page($this->presenter->present($nodes, $depth)); } /** * Lex and parse a string of code into statements. * * @param Parser $parser * @param string $code * * @return array Statements */ private function parse(Parser $parser, $code) { try { return $parser->parse($code); } catch (\PhpParser\Error $e) { if (strpos($e->getMessage(), 'unexpected EOF') === false) { throw $e; } // If we got an unexpected EOF, let's try it again with a semicolon. return $parser->parse($code . ';'); } } /** * Get (or create) the Parser instance. * * @param string|null $kind One of Psy\ParserFactory constants (only for PHP parser 2.0 and above) * * @return Parser */ private function getParser($kind = null) { if (!array_key_exists($kind, $this->parsers)) { $this->parsers[$kind] = $this->parserFactory->createParser($kind); } return $this->parsers[$kind]; } } setName('version') ->setDefinition(array()) ->setDescription('Show Psy Shell version.') ->setHelp('Show Psy Shell version.'); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $output->writeln($this->getApplication()->getVersion()); } } )(\w+)$/'; const INSTANCE_STATIC = '/^\$(\w+)::\$(\w+)$/'; /** * Context instance (for ContextAware interface). * * @var Context */ protected $context; /** * ContextAware interface. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } /** * Get the target for a value. * * @throws \InvalidArgumentException when the value specified can't be resolved * * @param string $valueName Function, class, variable, constant, method or property name * @param bool $classOnly True if the name should only refer to a class, function or instance * * @return array (class or instance name, member name, kind) */ protected function getTarget($valueName, $classOnly = false) { $valueName = trim($valueName); $matches = array(); switch (true) { case preg_match(self::CLASS_OR_FUNC, $valueName, $matches): return array($this->resolveName($matches[0], true), null, 0); case preg_match(self::INSTANCE, $valueName, $matches): return array($this->resolveInstance($matches[1]), null, 0); case !$classOnly && preg_match(self::CLASS_MEMBER, $valueName, $matches): return array($this->resolveName($matches[1]), $matches[2], Mirror::CONSTANT | Mirror::METHOD); case !$classOnly && preg_match(self::CLASS_STATIC, $valueName, $matches): return array($this->resolveName($matches[1]), $matches[2], Mirror::STATIC_PROPERTY | Mirror::PROPERTY); case !$classOnly && preg_match(self::INSTANCE_MEMBER, $valueName, $matches): if ($matches[2] === '->') { $kind = Mirror::METHOD | Mirror::PROPERTY; } else { $kind = Mirror::CONSTANT | Mirror::METHOD; } return array($this->resolveInstance($matches[1]), $matches[3], $kind); case !$classOnly && preg_match(self::INSTANCE_STATIC, $valueName, $matches): return array($this->resolveInstance($matches[1]), $matches[2], Mirror::STATIC_PROPERTY); default: throw new RuntimeException('Unknown target: ' . $valueName); } } /** * Resolve a class or function name (with the current shell namespace). * * @param string $name * @param bool $includeFunctions (default: false) * * @return string */ protected function resolveName($name, $includeFunctions = false) { if (substr($name, 0, 1) === '\\') { return $name; } if ($namespace = $this->getApplication()->getNamespace()) { $fullName = $namespace . '\\' . $name; if (class_exists($fullName) || interface_exists($fullName) || ($includeFunctions && function_exists($fullName))) { return $fullName; } } return $name; } /** * Get a Reflector and documentation for a function, class or instance, constant, method or property. * * @param string $valueName Function, class, variable, constant, method or property name * @param bool $classOnly True if the name should only refer to a class, function or instance * * @return array (value, Reflector) */ protected function getTargetAndReflector($valueName, $classOnly = false) { list($value, $member, $kind) = $this->getTarget($valueName, $classOnly); return array($value, Mirror::get($value, $member, $kind)); } /** * Return a variable instance from the current scope. * * @throws \InvalidArgumentException when the requested variable does not exist in the current scope * * @param string $name * * @return mixed Variable instance */ protected function resolveInstance($name) { $value = $this->getScopeVariable($name); if (!is_object($value)) { throw new RuntimeException('Unable to inspect a non-object'); } return $value; } /** * Get a variable from the current shell scope. * * @param string $name * * @return mixed */ protected function getScopeVariable($name) { return $this->context->get($name); } /** * Get all scope variables from the current shell scope. * * @return array */ protected function getScopeVariables() { return $this->context->getAll(); } } colorMode = $colorMode ?: Configuration::COLOR_MODE_AUTO; return parent::__construct(); } /** * {@inheritdoc} */ protected function configure() { $this ->setName('show') ->setDefinition(array( new InputArgument('value', InputArgument::REQUIRED, 'Function, class, instance, constant, method or property to show.'), )) ->setDescription('Show the code for an object, class, constant, method or property.') ->setHelp( <<>>> show \$myObject >>> show Psy\Shell::debug HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { list($value, $reflector) = $this->getTargetAndReflector($input->getArgument('value')); try { $output->page(CodeFormatter::format($reflector, $this->colorMode), ShellOutput::OUTPUT_RAW); } catch (RuntimeException $e) { $output->writeln(SignatureFormatter::format($reflector)); throw $e; } } } context = $context; } /** * {@inheritdoc} */ protected function configure() { $this ->setName('throw-up') ->setDefinition(array( new InputArgument('exception', InputArgument::OPTIONAL, 'Exception to throw'), )) ->setDescription('Throw an exception out of the Psy Shell.') ->setHelp( <<<'HELP' Throws an exception out of the current the Psy Shell instance. By default it throws the most recent exception. e.g. >>> throw-up >>> throw-up $e HELP ); } /** * {@inheritdoc} * * @throws InvalidArgumentException if there is no exception to throw * @throws ThrowUpException because what else do you expect it to do? */ protected function execute(InputInterface $input, OutputInterface $output) { if ($name = $input->getArgument('exception')) { $orig = $this->context->get(preg_replace('/^\$/', '', $name)); } else { $orig = $this->context->getLastException(); } if (!$orig instanceof \Exception) { throw new \InvalidArgumentException('throw-up can only throw Exceptions'); } throw new ThrowUpException($orig); } } setName('trace') ->setDefinition(array( new InputOption('include-psy', 'p', InputOption::VALUE_NONE, 'Include Psy in the call stack.'), new InputOption('num', 'n', InputOption::VALUE_REQUIRED, 'Only include NUM lines.'), )) ->setDescription('Show the current call stack.') ->setHelp( <<<'HELP' Show the current call stack. Optionally, include PsySH in the call stack by passing the --include-psy option. e.g. > trace -n10 > trace --include-psy HELP ); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $trace = $this->getBacktrace(new \Exception(), $input->getOption('num'), $input->getOption('include-psy')); $output->page($trace, ShellOutput::NUMBER_LINES); } /** * Get a backtrace for an exception. * * Optionally limit the number of rows to include with $count, and exclude * Psy from the trace. * * @param \Exception $e The exception with a backtrace * @param int $count (default: PHP_INT_MAX) * @param bool $includePsy (default: true) * * @return array Formatted stacktrace lines */ protected function getBacktrace(\Exception $e, $count = null, $includePsy = true) { if ($cwd = getcwd()) { $cwd = rtrim($cwd, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } if ($count === null) { $count = PHP_INT_MAX; } $lines = array(); $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(), )); if (!$includePsy) { for ($i = count($trace) - 1; $i >= 0; $i--) { $thing = isset($trace[$i]['class']) ? $trace[$i]['class'] : $trace[$i]['function']; if (preg_match('/\\\\?Psy\\\\/', $thing)) { $trace = array_slice($trace, $i + 1); break; } } } for ($i = 0, $count = min($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']) ? $this->replaceCwd($cwd, $trace[$i]['file']) : 'n/a'; $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; $lines[] = sprintf( ' %s%s%s() at %s:%s', OutputFormatter::escape($class), OutputFormatter::escape($type), OutputFormatter::escape($function), OutputFormatter::escape($file), OutputFormatter::escape($line) ); } return $lines; } /** * Replace the given directory from the start of a filepath. * * @param string $cwd * @param string $file * * @return string */ private function replaceCwd($cwd, $file) { if ($cwd === false) { return $file; } else { return preg_replace('/^' . preg_quote($cwd, '/') . '/', '', $file); } } } colorMode = $colorMode ?: Configuration::COLOR_MODE_AUTO; if (version_compare(PHP_VERSION, '5.3.6', '>=')) { $this->backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); } else { $this->backtrace = debug_backtrace(); } return parent::__construct(); } /** * {@inheritdoc} */ protected function configure() { $this ->setName('whereami') ->setDefinition(array( new InputOption('num', 'n', InputOption::VALUE_OPTIONAL, 'Number of lines before and after.', '5'), )) ->setDescription('Show where you are in the code.') ->setHelp( <<<'HELP' Show where you are in the code. Optionally, include how many lines before and after you want to display. e.g. > whereami > whereami -n10 HELP ); } /** * Obtains the correct trace in the full backtrace. * * @return array */ protected function trace() { foreach ($this->backtrace as $i => $backtrace) { if (!isset($backtrace['class'], $backtrace['function'])) { continue; } $correctClass = $backtrace['class'] === 'Psy\Shell'; $correctFunction = $backtrace['function'] === 'debug'; if ($correctClass && $correctFunction) { return $backtrace; } } return end($this->backtrace); } /** * Determine the file and line based on the specific backtrace. * * @return array */ protected function fileInfo() { $backtrace = $this->trace(); if (preg_match('/eval\(/', $backtrace['file'])) { preg_match_all('/([^\(]+)\((\d+)/', $backtrace['file'], $matches); $file = $matches[1][0]; $line = (int) $matches[2][0]; } else { $file = $backtrace['file']; $line = $backtrace['line']; } return compact('file', 'line'); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $info = $this->fileInfo(); $num = $input->getOption('num'); $factory = new ConsoleColorFactory($this->colorMode); $colors = $factory->getConsoleColor(); $highlighter = new Highlighter($colors); $contents = file_get_contents($info['file']); $output->page($highlighter->getCodeSnippet($contents, $info['line'], $num, $num), ShellOutput::OUTPUT_RAW); } } context = $context; } /** * {@inheritdoc} */ protected function configure() { $this ->setName('wtf') ->setAliases(array('last-exception', 'wtf?')) ->setDefinition(array( new InputArgument('incredulity', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Number of lines to show'), new InputOption('verbose', 'v', InputOption::VALUE_NONE, 'Show entire backtrace.'), )) ->setDescription('Show the backtrace of the most recent exception.') ->setHelp( <<<'HELP' Shows a few lines of the backtrace of the most recent exception. If you want to see more lines, add more question marks or exclamation marks: e.g. >>> wtf ? >>> wtf ?!???!?!? To see the entire backtrace, pass the -v/--verbose flag: e.g. >>> wtf -v HELP ); } /** * {@inheritdoc} * * --verbose is not hidden for this option :) * * @return array */ protected function getHiddenOptions() { $options = parent::getHiddenOptions(); unset($options[array_search('verbose', $options)]); return $options; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $incredulity = implode('', $input->getArgument('incredulity')); if (strlen(preg_replace('/[\\?!]/', '', $incredulity))) { throw new \InvalidArgumentException('Incredulity must include only "?" and "!".'); } $exception = $this->context->getLastException(); $count = $input->getOption('verbose') ? PHP_INT_MAX : pow(2, max(0, (strlen($incredulity) - 1))); $trace = $this->getBacktrace($exception, $count); $shell = $this->getApplication(); $output->page(function ($output) use ($exception, $trace, $shell) { $shell->renderException($exception, $output); $output->writeln('--'); $output->write($trace, true, ShellOutput::NUMBER_LINES); }); } } version = Shell::VERSION; $phar = new \Phar($pharFile, 0, 'psysh.phar'); $phar->setSignatureAlgorithm(\Phar::SHA1); $phar->startBuffering(); $finder = Finder::create() ->files() ->ignoreVCS(true) ->name('*.php') ->notName('Compiler.php') ->notName('Autoloader.php') ->in(__DIR__ . '/..'); foreach ($finder as $file) { $this->addFile($phar, $file); } $finder = Finder::create() ->files() ->ignoreVCS(true) ->name('*.php') ->exclude('Tests') ->exclude('tests') ->exclude('Test') ->exclude('test') ->in(__DIR__ . '/../../build-vendor'); foreach ($finder as $file) { $this->addFile($phar, $file); } // Stubs $phar->setStub($this->getStub()); $phar->stopBuffering(); unset($phar); } /** * Add a file to the psysh Phar. * * @param Phar $phar * @param SplFileInfo $file * @param bool $strip (default: true) */ private function addFile($phar, $file, $strip = true) { $path = str_replace(dirname(dirname(__DIR__)) . DIRECTORY_SEPARATOR, '', $file->getRealPath()); $content = file_get_contents($file); if ($strip) { $content = $this->stripWhitespace($content); } elseif ('LICENSE' === basename($file)) { $content = "\n" . $content . "\n"; } $phar->addFromString($path, $content); } /** * Removes whitespace from a PHP source string while preserving line numbers. * * @param string $source A PHP string * * @return string The PHP string with the whitespace removed */ private function stripWhitespace($source) { if (!function_exists('token_get_all')) { return $source; } $output = ''; foreach (token_get_all($source) as $token) { if (is_string($token)) { $output .= $token; } elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) { $output .= str_repeat("\n", substr_count($token[1], "\n")); } elseif (T_WHITESPACE === $token[0]) { // reduce wide spaces $whitespace = preg_replace('{[ \t]+}', ' ', $token[1]); // normalize newlines to \n $whitespace = preg_replace('{(?:\r\n|\r|\n)}', "\n", $whitespace); // trim leading spaces $whitespace = preg_replace('{\n +}', "\n", $whitespace); $output .= $whitespace; } else { $output .= $token[1]; } } return $output; } private static function getStubLicense() { $license = file_get_contents(__DIR__ . '/../../LICENSE'); $license = str_replace('The MIT License (MIT)', '', $license); $license = str_replace("\n", "\n * ", trim($license)); return $license; } const STUB_AUTOLOAD = <<<'EOS' Phar::mapPhar('psysh.phar'); require 'phar://psysh.phar/build-vendor/autoload.php'; EOS; /** * Get a Phar stub for psysh. * * This is basically the psysh bin, with the autoload require statements swapped out. * * @return string */ private function getStub() { $content = file_get_contents(__DIR__ . '/../../bin/psysh'); if (version_compare(PHP_VERSION, '5.4', '<')) { $content = str_replace('#!/usr/bin/env php', '#!/usr/bin/env php -d detect_unicode=Off', $content); } $content = preg_replace('{/\* <<<.*?>>> \*/}sm', self::STUB_AUTOLOAD, $content); $content = preg_replace('/\\(c\\) .*?with this source code./sm', self::getStubLicense(), $content); $content .= '__HALT_COMPILER();'; return $content; } } getConfigDirs()); } /** * Get potential home config directory paths. * * Returns `~/.psysh`, `%APPDATA%/PsySH` (when on Windows), and the * XDG Base Directory home config directory: * * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html * * @return string[] */ public static function getHomeConfigDirs() { $xdg = new Xdg(); return self::getDirNames(array($xdg->getHomeConfigDir())); } /** * Get the current home config directory. * * Returns the highest precedence home config directory which actually * exists. If none of them exists, returns the highest precedence home * config directory (`%APPDATA%/PsySH` on Windows, `~/.config/psysh` * everywhere else). * * @see self::getHomeConfigDirs * * @return string */ public static function getCurrentConfigDir() { $configDirs = self::getHomeConfigDirs(); foreach ($configDirs as $configDir) { if (@is_dir($configDir)) { return $configDir; } } return $configDirs[0]; } /** * Find real config files in config directories. * * @param string[] $names Config file names * @param string $configDir Optionally use a specific config directory * * @return string[] */ public static function getConfigFiles(array $names, $configDir = null) { $dirs = ($configDir === null) ? self::getConfigDirs() : array($configDir); return self::getRealFiles($dirs, $names); } /** * Get potential data directory paths. * * If a `dataDir` option was explicitly set, returns an array containing * just that directory. * * Otherwise, it returns `~/.psysh` and all XDG Base Directory data directories: * * http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html * * @return string[] */ public static function getDataDirs() { $xdg = new Xdg(); return self::getDirNames($xdg->getDataDirs()); } /** * Find real data files in config directories. * * @param string[] $names Config file names * @param string $dataDir Optionally use a specific config directory * * @return string[] */ public static function getDataFiles(array $names, $dataDir = null) { $dirs = ($dataDir === null) ? self::getDataDirs() : array($dataDir); return self::getRealFiles($dirs, $names); } /** * Get a runtime directory. * * Defaults to `/psysh` inside the system's temp dir. * * @return string */ public static function getRuntimeDir() { $xdg = new Xdg(); return $xdg->getRuntimeDir(false) . '/psysh'; } private static function getDirNames(array $baseDirs) { $dirs = array_map(function ($dir) { return strtr($dir, '\\', '/') . '/psysh'; }, $baseDirs); // Add ~/.psysh if ($home = getenv('HOME')) { $dirs[] = strtr($home, '\\', '/') . '/.psysh'; } // Add some Windows specific ones :) if (defined('PHP_WINDOWS_VERSION_MAJOR')) { if ($appData = getenv('APPDATA')) { // AppData gets preference array_unshift($dirs, strtr($appData, '\\', '/') . '/PsySH'); } $dir = strtr(getenv('HOMEDRIVE') . '/' . getenv('HOMEPATH'), '\\', '/') . '/.psysh'; if (!in_array($dir, $dirs)) { $dirs[] = $dir; } } return $dirs; } private static function getRealFiles(array $dirNames, array $fileNames) { $files = array(); foreach ($dirNames as $dir) { foreach ($fileNames as $name) { $file = $dir . '/' . $name; if (@is_file($file)) { $files[] = $file; } } } return $files; } } setColorMode(self::COLOR_MODE_AUTO); // explicit configFile option if (isset($config['configFile'])) { $this->configFile = $config['configFile']; } elseif ($configFile = getenv('PSYSH_CONFIG')) { $this->configFile = $configFile; } // legacy baseDir option if (isset($config['baseDir'])) { $msg = "The 'baseDir' configuration option is deprecated. " . "Please specify 'configDir' and 'dataDir' options instead."; throw new DeprecatedException($msg); } unset($config['configFile'], $config['baseDir']); // go go gadget, config! $this->loadConfig($config); $this->init(); } /** * Initialize the configuration. * * This checks for the presence of Readline and Pcntl extensions. * * If a config file is available, it will be loaded and merged with the current config. * * If no custom config file was specified and a local project config file * is available, it will be loaded and merged with the current config. */ public function init() { // feature detection $this->hasReadline = function_exists('readline'); $this->hasPcntl = function_exists('pcntl_signal') && function_exists('posix_getpid'); if ($configFile = $this->getConfigFile()) { $this->loadConfigFile($configFile); } if (!$this->configFile && $localConfig = $this->getLocalConfigFile()) { $this->loadConfigFile($localConfig); } } /** * Get the current PsySH config file. * * If a `configFile` option was passed to the Configuration constructor, * this file will be returned. If not, all possible config directories will * be searched, and the first `config.php` or `rc.php` file which exists * will be returned. * * If you're trying to decide where to put your config file, pick * * ~/.config/psysh/config.php * * @return string */ public function getConfigFile() { if (isset($this->configFile)) { return $this->configFile; } $files = ConfigPaths::getConfigFiles(array('config.php', 'rc.php'), $this->configDir); if (!empty($files)) { if ($this->warnOnMultipleConfigs && count($files) > 1) { $msg = sprintf('Multiple configuration files found: %s. Using %s', implode($files, ', '), $files[0]); trigger_error($msg, E_USER_NOTICE); } return $files[0]; } } /** * Get the local PsySH config file. * * Searches for a project specific config file `.psysh.php` in the current * working directory. * * @return string */ public function getLocalConfigFile() { $localConfig = getenv('PWD') . '/.psysh.php'; if (@is_file($localConfig)) { return $localConfig; } } /** * Load configuration values from an array of options. * * @param array $options */ public function loadConfig(array $options) { foreach (self::$AVAILABLE_OPTIONS as $option) { if (isset($options[$option])) { $method = 'set' . ucfirst($option); $this->$method($options[$option]); } } foreach (array('commands', 'tabCompletionMatchers', 'casters') as $option) { if (isset($options[$option])) { $method = 'add' . ucfirst($option); $this->$method($options[$option]); } } } /** * Load a configuration file (default: `$HOME/.config/psysh/config.php`). * * This configuration instance will be available to the config file as $config. * The config file may directly manipulate the configuration, or may return * an array of options which will be merged with the current configuration. * * @throws \InvalidArgumentException if the config file returns a non-array result * * @param string $file */ public function loadConfigFile($file) { $__psysh_config_file__ = $file; $load = function ($config) use ($__psysh_config_file__) { $result = require $__psysh_config_file__; if ($result !== 1) { return $result; } }; $result = $load($this); if (!empty($result)) { if (is_array($result)) { $this->loadConfig($result); } else { throw new \InvalidArgumentException('Psy Shell configuration must return an array of options'); } } } /** * Set files to be included by default at the start of each shell session. * * @param array $includes */ public function setDefaultIncludes(array $includes = array()) { $this->defaultIncludes = $includes; } /** * Get files to be included by default at the start of each shell session. * * @return array */ public function getDefaultIncludes() { return $this->defaultIncludes ?: array(); } /** * Set the shell's config directory location. * * @param string $dir */ public function setConfigDir($dir) { $this->configDir = (string) $dir; } /** * Get the current configuration directory, if any is explicitly set. * * @return string */ public function getConfigDir() { return $this->configDir; } /** * Set the shell's data directory location. * * @param string $dir */ public function setDataDir($dir) { $this->dataDir = (string) $dir; } /** * Get the current data directory, if any is explicitly set. * * @return string */ public function getDataDir() { return $this->dataDir; } /** * Set the shell's temporary directory location. * * @param string $dir */ public function setRuntimeDir($dir) { $this->runtimeDir = (string) $dir; } /** * Get the shell's temporary directory location. * * Defaults to `/psysh` inside the system's temp dir unless explicitly * overridden. * * @return string */ public function getRuntimeDir() { if (!isset($this->runtimeDir)) { $this->runtimeDir = ConfigPaths::getRuntimeDir(); } if (!is_dir($this->runtimeDir)) { mkdir($this->runtimeDir, 0700, true); } return $this->runtimeDir; } /** * Set the readline history file path. * * @param string $file */ public function setHistoryFile($file) { $this->historyFile = (string) $file; } /** * Get the readline history file path. * * Defaults to `/history` inside the shell's base config dir unless * explicitly overridden. * * @return string */ public function getHistoryFile() { if (isset($this->historyFile)) { return $this->historyFile; } // Deprecation warning for incorrect psysh_history path. // TODO: remove this before v0.8.0 $xdg = new Xdg(); $oldHistory = $xdg->getHomeConfigDir() . '/psysh_history'; if (@is_file($oldHistory)) { $dir = $this->configDir ?: ConfigPaths::getCurrentConfigDir(); $newHistory = $dir . '/psysh_history'; $msg = sprintf( "PsySH history file found at '%s'. Please delete it or move it to '%s'.", strtr($oldHistory, '\\', '/'), $newHistory ); @trigger_error($msg, E_USER_DEPRECATED); return $this->historyFile = $oldHistory; } $files = ConfigPaths::getConfigFiles(array('psysh_history', 'history'), $this->configDir); if (!empty($files)) { if ($this->warnOnMultipleConfigs && count($files) > 1) { $msg = sprintf('Multiple history files found: %s. Using %s', implode($files, ', '), $files[0]); trigger_error($msg, E_USER_NOTICE); } return $this->historyFile = $files[0]; } // fallback: create our own history file $dir = $this->configDir ?: ConfigPaths::getCurrentConfigDir(); if (!is_dir($dir)) { mkdir($dir, 0700, true); } return $this->historyFile = $dir . '/psysh_history'; } /** * Set the readline max history size. * * @param int $value */ public function setHistorySize($value) { $this->historySize = (int) $value; } /** * Get the readline max history size. * * @return int */ public function getHistorySize() { return $this->historySize; } /** * Sets whether readline erases old duplicate history entries. * * @param bool $value */ public function setEraseDuplicates($value) { $this->eraseDuplicates = (bool) $value; } /** * Get whether readline erases old duplicate history entries. * * @return bool */ public function getEraseDuplicates() { return $this->eraseDuplicates; } /** * Get a temporary file of type $type for process $pid. * * The file will be created inside the current temporary directory. * * @see self::getRuntimeDir * * @param string $type * @param int $pid * * @return string Temporary file name */ public function getTempFile($type, $pid) { return tempnam($this->getRuntimeDir(), $type . '_' . $pid . '_'); } /** * Get a filename suitable for a FIFO pipe of $type for process $pid. * * The pipe will be created inside the current temporary directory. * * @param string $type * @param id $pid * * @return string Pipe name */ public function getPipe($type, $pid) { return sprintf('%s/%s_%s', $this->getRuntimeDir(), $type, $pid); } /** * Check whether this PHP instance has Readline available. * * @return bool True if Readline is available */ public function hasReadline() { return $this->hasReadline; } /** * Enable or disable Readline usage. * * @param bool $useReadline */ public function setUseReadline($useReadline) { $this->useReadline = (bool) $useReadline; } /** * Check whether to use Readline. * * If `setUseReadline` as been set to true, but Readline is not actually * available, this will return false. * * @return bool True if the current Shell should use Readline */ public function useReadline() { return isset($this->useReadline) ? ($this->hasReadline && $this->useReadline) : $this->hasReadline; } /** * Set the Psy Shell readline service. * * @param Readline $readline */ public function setReadline(Readline $readline) { $this->readline = $readline; } /** * Get the Psy Shell readline service. * * By default, this service uses (in order of preference): * * * GNU Readline * * Libedit * * A transient array-based readline emulation. * * @return Readline */ public function getReadline() { if (!isset($this->readline)) { $className = $this->getReadlineClass(); $this->readline = new $className( $this->getHistoryFile(), $this->getHistorySize(), $this->getEraseDuplicates() ); } return $this->readline; } /** * Get the appropriate Readline implementation class name. * * @see self::getReadline * * @return string */ private function getReadlineClass() { if ($this->useReadline()) { if (GNUReadline::isSupported()) { return 'Psy\Readline\GNUReadline'; } elseif (Libedit::isSupported()) { return 'Psy\Readline\Libedit'; } elseif (HoaConsole::isSupported()) { return 'Psy\Readline\HoaConsole'; } } return 'Psy\Readline\Transient'; } /** * Check whether this PHP instance has Pcntl available. * * @return bool True if Pcntl is available */ public function hasPcntl() { return $this->hasPcntl; } /** * Enable or disable Pcntl usage. * * @param bool $usePcntl */ public function setUsePcntl($usePcntl) { $this->usePcntl = (bool) $usePcntl; } /** * Check whether to use Pcntl. * * If `setUsePcntl` has been set to true, but Pcntl is not actually * available, this will return false. * * @return bool True if the current Shell should use Pcntl */ public function usePcntl() { return isset($this->usePcntl) ? ($this->hasPcntl && $this->usePcntl) : $this->hasPcntl; } /** * Enable or disable strict requirement of semicolons. * * @see self::requireSemicolons() * * @param bool $requireSemicolons */ public function setRequireSemicolons($requireSemicolons) { $this->requireSemicolons = (bool) $requireSemicolons; } /** * Check whether to require semicolons on all statements. * * By default, PsySH will automatically insert semicolons at the end of * statements if they're missing. To strictly require semicolons, set * `requireSemicolons` to true. * * @return bool */ public function requireSemicolons() { return $this->requireSemicolons; } /** * Enable or disable Unicode in PsySH specific output. * * Note that this does not disable Unicode output in general, it just makes * it so PsySH won't output any itself. * * @param bool $useUnicode */ public function setUseUnicode($useUnicode) { $this->useUnicode = (bool) $useUnicode; } /** * Check whether to use Unicode in PsySH specific output. * * Note that this does not disable Unicode output in general, it just makes * it so PsySH won't output any itself. * * @return bool */ public function useUnicode() { if (isset($this->useUnicode)) { return $this->useUnicode; } // TODO: detect `chsh` != 65001 on Windows and return false return true; } /** * Set the error logging level. * * @see self::errorLoggingLevel * * @param bool $errorLoggingLevel */ public function setErrorLoggingLevel($errorLoggingLevel) { $this->errorLoggingLevel = (E_ALL | E_STRICT) & $errorLoggingLevel; } /** * Get the current error logging level. * * By default, PsySH will automatically log all errors, regardless of the * current `error_reporting` level. Additionally, if the `error_reporting` * level warrants, an ErrorException will be thrown. * * Set `errorLoggingLevel` to 0 to prevent logging non-thrown errors. Set it * to any valid error_reporting value to log only errors which match that * level. * * http://php.net/manual/en/function.error-reporting.php * * @return int */ public function errorLoggingLevel() { return $this->errorLoggingLevel; } /** * Set a CodeCleaner service instance. * * @param CodeCleaner $cleaner */ public function setCodeCleaner(CodeCleaner $cleaner) { $this->cleaner = $cleaner; } /** * Get a CodeCleaner service instance. * * If none has been explicitly defined, this will create a new instance. * * @return CodeCleaner */ public function getCodeCleaner() { if (!isset($this->cleaner)) { $this->cleaner = new CodeCleaner(); } return $this->cleaner; } /** * Enable or disable tab completion. * * @param bool $tabCompletion */ public function setTabCompletion($tabCompletion) { $this->tabCompletion = (bool) $tabCompletion; } /** * Check whether to use tab completion. * * If `setTabCompletion` has been set to true, but readline is not actually * available, this will return false. * * @return bool True if the current Shell should use tab completion */ public function getTabCompletion() { return isset($this->tabCompletion) ? ($this->hasReadline && $this->tabCompletion) : $this->hasReadline; } /** * Set the Shell Output service. * * @param ShellOutput $output */ public function setOutput(ShellOutput $output) { $this->output = $output; } /** * Get a Shell Output service instance. * * If none has been explicitly provided, this will create a new instance * with VERBOSITY_NORMAL and the output page supplied by self::getPager * * @see self::getPager * * @return ShellOutput */ public function getOutput() { if (!isset($this->output)) { $this->output = new ShellOutput( ShellOutput::VERBOSITY_NORMAL, $this->getOutputDecorated(), null, $this->getPager() ); } return $this->output; } /** * Get the decoration (i.e. color) setting for the Shell Output service. * * @return null|bool 3-state boolean corresponding to the current color mode */ public function getOutputDecorated() { if ($this->colorMode() === self::COLOR_MODE_AUTO) { return; } elseif ($this->colorMode() === self::COLOR_MODE_FORCED) { return true; } elseif ($this->colorMode() === self::COLOR_MODE_DISABLED) { return false; } } /** * Set the OutputPager service. * * If a string is supplied, a ProcOutputPager will be used which shells out * to the specified command. * * @throws \InvalidArgumentException if $pager is not a string or OutputPager instance * * @param string|OutputPager $pager */ public function setPager($pager) { if ($pager && !is_string($pager) && !$pager instanceof OutputPager) { throw new \InvalidArgumentException('Unexpected pager instance.'); } $this->pager = $pager; } /** * Get an OutputPager instance or a command for an external Proc pager. * * If no Pager has been explicitly provided, and Pcntl is available, this * will default to `cli.pager` ini value, falling back to `which less`. * * @return string|OutputPager */ public function getPager() { if (!isset($this->pager) && $this->usePcntl()) { if ($pager = ini_get('cli.pager')) { // use the default pager (5.4+) $this->pager = $pager; } elseif ($less = exec('which less 2>/dev/null')) { // check for the presence of less... $this->pager = $less . ' -R -S -F -X'; } } return $this->pager; } /** * Set the Shell evaluation Loop service. * * @param Loop $loop */ public function setLoop(Loop $loop) { $this->loop = $loop; } /** * Get a Shell evaluation Loop service instance. * * If none has been explicitly defined, this will create a new instance. * If Pcntl is available and enabled, the new instance will be a ForkingLoop. * * @return Loop */ public function getLoop() { if (!isset($this->loop)) { if ($this->usePcntl()) { $this->loop = new ForkingLoop($this); } else { $this->loop = new Loop($this); } } return $this->loop; } /** * Set the Shell autocompleter service. * * @param AutoCompleter $completer */ public function setAutoCompleter(AutoCompleter $completer) { $this->completer = $completer; } /** * Get an AutoCompleter service instance. * * @return AutoCompleter */ public function getAutoCompleter() { if (!isset($this->completer)) { $this->completer = new AutoCompleter(); } return $this->completer; } /** * Get user specified tab completion matchers for the AutoCompleter. * * @return array */ public function getTabCompletionMatchers() { return $this->tabCompletionMatchers; } /** * Add additional tab completion matchers to the AutoCompleter. * * @param array $matchers */ public function addTabCompletionMatchers(array $matchers) { $this->tabCompletionMatchers = array_merge($this->tabCompletionMatchers, $matchers); if (isset($this->shell)) { $this->shell->addTabCompletionMatchers($this->tabCompletionMatchers); } } /** * Add commands to the Shell. * * This will buffer new commands in the event that the Shell has not yet * been instantiated. This allows the user to specify commands in their * config rc file, despite the fact that their file is needed in the Shell * constructor. * * @param array $commands */ public function addCommands(array $commands) { $this->newCommands = array_merge($this->newCommands, $commands); if (isset($this->shell)) { $this->doAddCommands(); } } /** * Internal method for adding commands. This will set any new commands once * a Shell is available. */ private function doAddCommands() { if (!empty($this->newCommands)) { $this->shell->addCommands($this->newCommands); $this->newCommands = array(); } } /** * Set the Shell backreference and add any new commands to the Shell. * * @param Shell $shell */ public function setShell(Shell $shell) { $this->shell = $shell; $this->doAddCommands(); } /** * Set the PHP manual database file. * * This file should be an SQLite database generated from the phpdoc source * with the `bin/build_manual` script. * * @param string $filename */ public function setManualDbFile($filename) { $this->manualDbFile = (string) $filename; } /** * Get the current PHP manual database file. * * @return string Default: '~/.local/share/psysh/php_manual.sqlite' */ public function getManualDbFile() { if (isset($this->manualDbFile)) { return $this->manualDbFile; } $files = ConfigPaths::getDataFiles(array('php_manual.sqlite'), $this->dataDir); if (!empty($files)) { if ($this->warnOnMultipleConfigs && count($files) > 1) { $msg = sprintf('Multiple manual database files found: %s. Using %s', implode($files, ', '), $files[0]); trigger_error($msg, E_USER_NOTICE); } return $this->manualDbFile = $files[0]; } } /** * Get a PHP manual database connection. * * @return PDO */ public function getManualDb() { if (!isset($this->manualDb)) { $dbFile = $this->getManualDbFile(); if (is_file($dbFile)) { try { $this->manualDb = new \PDO('sqlite:' . $dbFile); } catch (\PDOException $e) { if ($e->getMessage() === 'could not find driver') { throw new RuntimeException('SQLite PDO driver not found', 0, $e); } else { throw $e; } } } } return $this->manualDb; } /** * Add an array of casters definitions. * * @param array $casters */ public function addCasters(array $casters) { $this->getPresenter()->addCasters($casters); } /** * Get the Presenter service. * * @return Presenter */ public function getPresenter() { if (!isset($this->presenter)) { $this->presenter = new Presenter($this->getOutput()->getFormatter()); } return $this->presenter; } /** * Enable or disable warnings on multiple configuration or data files. * * @see self::warnOnMultipleConfigs() * * @param bool $warnOnMultipleConfigs */ public function setWarnOnMultipleConfigs($warnOnMultipleConfigs) { $this->warnOnMultipleConfigs = (bool) $warnOnMultipleConfigs; } /** * Check whether to warn on multiple configuration or data files. * * By default, PsySH will use the file with highest precedence, and will * silently ignore all others. With this enabled, a warning will be emitted * (but not an exception thrown) if multiple configuration or data files * are found. * * This will default to true in a future release, but is false for now. * * @return bool */ public function warnOnMultipleConfigs() { return $this->warnOnMultipleConfigs; } /** * Set the current color mode. * * @param string $colorMode */ public function setColorMode($colorMode) { $validColorModes = array( self::COLOR_MODE_AUTO, self::COLOR_MODE_FORCED, self::COLOR_MODE_DISABLED, ); if (in_array($colorMode, $validColorModes)) { $this->colorMode = $colorMode; } else { throw new \InvalidArgumentException('invalid color mode: ' . $colorMode); } } /** * Get the current color mode. * * @return string */ public function colorMode() { return $this->colorMode; } /** * Set an update checker service instance. * * @param Checker $checker */ public function setChecker(Checker $checker) { $this->checker = $checker; } /** * Get an update checker service instance. * * If none has been explicitly defined, this will create a new instance. * * @return Checker */ public function getChecker() { if (!isset($this->checker)) { $interval = $this->getUpdateCheck(); switch ($interval) { case Checker::ALWAYS: $this->checker = new GitHubChecker(); break; case Checker::DAILY: case Checker::WEEKLY: case Checker::MONTHLY: $this->checker = new IntervalChecker($this->getUpdateCheckCacheFile(), $interval); break; case Checker::NEVER: $this->checker = new NoopChecker(); break; } } return $this->checker; } /** * Get the current update check interval. * * One of 'always', 'daily', 'weekly', 'monthly' or 'never'. If none is * explicitly set, default to 'weekly'. * * @return string */ public function getUpdateCheck() { return isset($this->updateCheck) ? $this->updateCheck : Checker::WEEKLY; } /** * Set the update check interval. * * @throws \InvalidArgumentDescription if the update check interval is unknown * * @param string $interval */ public function setUpdateCheck($interval) { $validIntervals = array( Checker::ALWAYS, Checker::DAILY, Checker::WEEKLY, Checker::MONTHLY, Checker::NEVER, ); if (!in_array($interval, $validIntervals)) { throw new \InvalidArgumentException('invalid update check interval: ' . $interval); } $this->updateCheck = $interval; } /** * Get a cache file path for the update checker. * * @return string */ public function getUpdateCheckCacheFile() { $dir = $this->configDir ?: ConfigPaths::getCurrentConfigDir(); if (!is_dir($dir)) { mkdir($dir, 0700, true); } return $dir . '/update_check.json'; } } colorMode = $colorMode; } /** * Get a `ConsoleColor` instance configured according to the given color * mode. * * @return ConsoleColor */ public function getConsoleColor() { if ($this->colorMode === Configuration::COLOR_MODE_AUTO) { return $this->getDefaultConsoleColor(); } elseif ($this->colorMode === Configuration::COLOR_MODE_FORCED) { return $this->getForcedConsoleColor(); } elseif ($this->colorMode === Configuration::COLOR_MODE_DISABLED) { return $this->getDisabledConsoleColor(); } } private function getDefaultConsoleColor() { $color = new ConsoleColor(); $color->addTheme(Highlighter::LINE_NUMBER, array('blue')); return $color; } private function getForcedConsoleColor() { $color = $this->getDefaultConsoleColor(); $color->setForceStyle(true); return $color; } private function getDisabledConsoleColor() { $color = new ConsoleColor(); $color->addTheme(Highlighter::TOKEN_STRING, array('none')); $color->addTheme(Highlighter::TOKEN_COMMENT, array('none')); $color->addTheme(Highlighter::TOKEN_KEYWORD, array('none')); $color->addTheme(Highlighter::TOKEN_DEFAULT, array('none')); $color->addTheme(Highlighter::TOKEN_HTML, array('none')); $color->addTheme(Highlighter::ACTUAL_LINE_MARK, array('none')); $color->addTheme(Highlighter::LINE_NUMBER, array('none')); return $color; } } returnValue; case '_e': if (!isset($this->lastException)) { throw new \InvalidArgumentException('Unknown variable: $' . $name); } return $this->lastException; default: if (!array_key_exists($name, $this->scopeVariables)) { throw new \InvalidArgumentException('Unknown variable: $' . $name); } return $this->scopeVariables[$name]; } } /** * Get all defined variables. * * @return array */ public function getAll() { $vars = $this->scopeVariables; $vars['_'] = $this->returnValue; if (isset($this->lastException)) { $vars['_e'] = $this->lastException; } return $vars; } /** * Set all scope variables. * * This method does *not* set the magic $_ and $_e variables. * * @param array $vars */ public function setAll(array $vars) { foreach (self::$specialVars as $key) { unset($vars[$key]); } $this->scopeVariables = $vars; } /** * Set the most recent return value. * * @param mixed $value */ public function setReturnValue($value) { $this->returnValue = $value; } /** * Get the most recent return value. * * @return mixed */ public function getReturnValue() { return $this->returnValue; } /** * Set the most recent Exception. * * @param \Exception $e */ public function setLastException(\Exception $e) { $this->lastException = $e; } /** * Get the most recent Exception. * * @throws InvalidArgumentException If no Exception has been caught * * @return null|Exception */ public function getLastException() { if (!isset($this->lastException)) { throw new \InvalidArgumentException('No most-recent exception'); } return $this->lastException; } } rawMessage = $message; parent::__construct(sprintf('Exit: %s', $message), $code, $previous); } /** * Return a raw (unformatted) version of the error message. * * @return string */ public function getRawMessage() { return $this->rawMessage; } } rawMessage = $message; if (!empty($filename) && preg_match('{Psy[/\\\\]ExecutionLoop}', $filename)) { $filename = ''; } switch ($severity) { case E_WARNING: case E_CORE_WARNING: case E_COMPILE_WARNING: case E_USER_WARNING: $type = 'warning'; break; case E_STRICT: $type = 'Strict error'; break; default: $type = 'error'; break; } $message = sprintf('PHP %s: %s%s on line %d', $type, $message, $filename ? ' in ' . $filename : '', $lineno); parent::__construct($message, $code, $severity, $filename, $lineno, $previous); } /** * Get the raw (unformatted) message for this error. * * @return string */ public function getRawMessage() { return $this->rawMessage; } /** * Helper for throwing an ErrorException. * * This allows us to: * * set_error_handler(array('Psy\Exception\ErrorException', 'throwException')); * * @throws ErrorException * * @param int $errno Error type * @param string $errstr Message * @param string $errfile Filename * @param int $errline Line number */ public static function throwException($errno, $errstr, $errfile, $errline) { throw new self($errstr, 0, $errno, $errfile, $errline); } /** * Create an ErrorException from an Error. * * @param \Error $e * * @return ErrorException */ public static function fromError(\Error $e) { return new self($e->getMessage(), $e->getCode(), 1, $e->getFile(), $e->getLine(), $e); } } rawMessage = $message; $message = sprintf('PHP Fatal error: %s in %s on line %d', $message, $filename ?: "eval()'d code", $lineno); parent::__construct($message, $code, $severity, $filename, $lineno, $previous); } /** * Return a raw (unformatted) version of the error message. * * @return string */ public function getRawMessage() { return $this->rawMessage; } } getRawMessage(), $e->getStartLine()); } } rawMessage = $message; parent::__construct($message, $code, $previous); } /** * Return a raw (unformatted) version of the error message. * * @return string */ public function getRawMessage() { return $this->rawMessage; } } getMessage()); parent::__construct($message, $exception->getCode(), $exception); } /** * Return a raw (unformatted) version of the error message. * * @return string */ public function getRawMessage() { return $this->getPrevious()->getMessage(); } } rawMessage = $message; $message = preg_replace('/, called in .*?: eval\\(\\)\'d code/', '', $message); parent::__construct(sprintf('TypeError: %s', $message), $code); } /** * Get the raw (unformatted) message for this error. * * @return string */ public function getRawMessage() { return $this->rawMessage; } /** * Create a TypeErrorException from a TypeError. * * @param \TypeError $e * * @return TypeErrorException */ public static function fromTypeError(\TypeError $e) { return new self($e->getMessage(), $e->getLine()); } } 0) { // This is the main thread. We'll just wait for a while. // We won't be needing this one. fclose($up); // Wait for a return value from the loop process. $read = array($down); $write = null; $except = null; if (stream_select($read, $write, $except, null) === false) { throw new \RuntimeException('Error waiting for execution loop.'); } $content = stream_get_contents($down); fclose($down); if ($content) { $shell->setScopeVariables(@unserialize($content)); } return; } // This is the child process. It's going to do all the work. if (function_exists('setproctitle')) { setproctitle('psysh (loop)'); } // We won't be needing this one. fclose($down); // Let's do some processing. parent::run($shell); // Send the scope variables back up to the main thread fwrite($up, $this->serializeReturn($shell->getScopeVariables())); fclose($up); posix_kill(posix_getpid(), SIGKILL); } /** * Create a savegame at the start of each loop iteration. */ public function beforeLoop() { $this->createSavegame(); } /** * Clean up old savegames at the end of each loop iteration. */ public function afterLoop() { // if there's an old savegame hanging around, let's kill it. if (isset($this->savegame)) { posix_kill($this->savegame, SIGKILL); pcntl_signal_dispatch(); } } /** * Create a savegame fork. * * The savegame contains the current execution state, and can be resumed in * the event that the worker dies unexpectedly (for example, by encountering * a PHP fatal error). */ private function createSavegame() { // the current process will become the savegame $this->savegame = posix_getpid(); $pid = pcntl_fork(); if ($pid < 0) { throw new \RuntimeException('Unable to create savegame fork.'); } elseif ($pid > 0) { // we're the savegame now... let's wait and see what happens pcntl_waitpid($pid, $status); // worker exited cleanly, let's bail if (!pcntl_wexitstatus($status)) { posix_kill(posix_getpid(), SIGKILL); } // worker didn't exit cleanly, we'll need to have another go $this->createSavegame(); } } /** * Serialize all serializable return values. * * A naïve serialization will run into issues if there is a Closure or * SimpleXMLElement (among other things) in scope when exiting the execution * loop. We'll just ignore these unserializable classes, and serialize what * we can. * * @param array $return * * @return string */ private function serializeReturn(array $return) { $serializable = array(); foreach ($return as $key => $value) { // No need to return magic variables if ($key === '_' || $key === '_e') { continue; } // Resources don't error, but they don't serialize well either. if (is_resource($value) || $value instanceof \Closure) { continue; } try { @serialize($value); $serializable[$key] = $value; } catch (\Exception $e) { // we'll just ignore this one... } } return @serialize($serializable); } } getIncludes() as $__psysh_include__) { include $__psysh_include__; } } catch (\Exception $_e) { $__psysh__->writeException($_e); } restore_error_handler(); unset($__psysh_include__); extract($__psysh__->getScopeVariables()); do { $__psysh__->beforeLoop(); $__psysh__->setScopeVariables(get_defined_vars()); try { // read a line, see if we should eval $__psysh__->getInput(); // evaluate the current code buffer ob_start( array($__psysh__, 'writeStdout'), version_compare(PHP_VERSION, '5.4', '>=') ? 1 : 2 ); set_error_handler(array($__psysh__, 'handleError')); $_ = eval($__psysh__->flushCode() ?: Loop::NOOP_INPUT); restore_error_handler(); ob_end_flush(); $__psysh__->writeReturnValue($_); } catch (BreakException $_e) { restore_error_handler(); if (ob_get_level() > 0) { ob_end_clean(); } $__psysh__->writeException($_e); return; } catch (ThrowUpException $_e) { restore_error_handler(); if (ob_get_level() > 0) { ob_end_clean(); } $__psysh__->writeException($_e); throw $_e; } catch (\TypeError $_e) { restore_error_handler(); if (ob_get_level() > 0) { ob_end_clean(); } $__psysh__->writeException(TypeErrorException::fromTypeError($_e)); } catch (\Error $_e) { restore_error_handler(); if (ob_get_level() > 0) { ob_end_clean(); } $__psysh__->writeException(ErrorException::fromError($_e)); } catch (\Exception $_e) { restore_error_handler(); if (ob_get_level() > 0) { ob_end_clean(); } $__psysh__->writeException($_e); } $__psysh__->afterLoop(); } while (true); }; // bind the closure to $this from the shell scope variables... if (self::bindLoop()) { $that = null; try { $that = $shell->getScopeVariable('this'); } catch (\InvalidArgumentException $e) { // well, it was worth a shot } if (is_object($that)) { $loop = $loop->bindTo($that, get_class($that)); } else { $loop = $loop->bindTo(null, null); } } $loop($shell); } /** * A beforeLoop callback. * * This is executed at the start of each loop iteration. In the default * (non-forking) loop implementation, this is a no-op. */ public function beforeLoop() { // no-op } /** * A afterLoop callback. * * This is executed at the end of each loop iteration. In the default * (non-forking) loop implementation, this is a no-op. */ public function afterLoop() { // no-op } /** * Decide whether to bind the execution loop. * * @return bool */ protected static function bindLoop() { // skip binding on HHVM <= 3.5.0 // see https://github.com/facebook/hhvm/issues/1203 if (defined('HHVM_VERSION')) { return version_compare(HHVM_VERSION, '3.5.0', '>='); } return version_compare(PHP_VERSION, '5.4', '>='); } } getFileName()) { if (!is_file($fileName)) { throw new RuntimeException('Source code unavailable.'); } $file = file_get_contents($fileName); $start = $reflector->getStartLine(); $end = $reflector->getEndLine() - $start; $factory = new ConsoleColorFactory($colorMode); $colors = $factory->getConsoleColor(); $highlighter = new Highlighter($colors); return $highlighter->getCodeSnippet($file, $start, 0, $end); // no need to escape this bad boy, since (for now) it's being output raw. // return OutputFormatter::escape(implode(PHP_EOL, $code)); return implode(PHP_EOL, $code); } else { throw new RuntimeException('Source code unavailable.'); } } } 'info', 'var' => 'strong', ); /** * Format a docblock. * * @param \Reflector $reflector * * @return string Formatted docblock */ public static function format(\Reflector $reflector) { $docblock = new Docblock($reflector); $chunks = array(); if (!empty($docblock->desc)) { $chunks[] = 'Description:'; $chunks[] = self::indent(OutputFormatter::escape($docblock->desc), ' '); $chunks[] = ''; } if (!empty($docblock->tags)) { foreach ($docblock::$vectors as $name => $vector) { if (isset($docblock->tags[$name])) { $chunks[] = sprintf('%s:', self::inflect($name)); $chunks[] = self::formatVector($vector, $docblock->tags[$name]); $chunks[] = ''; } } $tags = self::formatTags(array_keys($docblock::$vectors), $docblock->tags); if (!empty($tags)) { $chunks[] = $tags; $chunks[] = ''; } } return rtrim(implode("\n", $chunks)); } /** * Format a docblock vector, for example, `@throws`, `@param`, or `@return`. * * @see DocBlock::$vectors * * @param array $vector * @param array $lines * * @return string */ private static function formatVector(array $vector, array $lines) { $template = array(' '); foreach ($vector as $type) { $max = 0; foreach ($lines as $line) { $chunk = $line[$type]; $cur = empty($chunk) ? 0 : strlen($chunk) + 1; if ($cur > $max) { $max = $cur; } } $template[] = self::getVectorParamTemplate($type, $max); } $template = implode(' ', $template); return implode("\n", array_map(function ($line) use ($template) { $escaped = array_map(array('Symfony\Component\Console\Formatter\OutputFormatter', 'escape'), $line); return rtrim(vsprintf($template, $escaped)); }, $lines)); } /** * Format docblock tags. * * @param array $skip Tags to exclude * @param array $tags Tags to format * * @return string formatted tags */ private static function formatTags(array $skip, array $tags) { $chunks = array(); foreach ($tags as $name => $values) { if (in_array($name, $skip)) { continue; } foreach ($values as $value) { $chunks[] = sprintf('%s%s %s', self::inflect($name), empty($value) ? '' : ':', OutputFormatter::escape($value)); } $chunks[] = ''; } return implode("\n", $chunks); } /** * Get a docblock vector template. * * @param string $type Vector type * @param int $max Pad width * * @return string */ private static function getVectorParamTemplate($type, $max) { if (!isset(self::$vectorParamTemplates[$type])) { return sprintf('%%-%ds', $max); } return sprintf('<%s>%%-%ds', self::$vectorParamTemplates[$type], $max, self::$vectorParamTemplates[$type]); } /** * Indent a string. * * @param string $text String to indent * @param string $indent (default: ' ') * * @return string */ private static function indent($text, $indent = ' ') { return $indent . str_replace("\n", "\n" . $indent, $text); } /** * Convert underscored or whitespace separated words into sentence case. * * @param string $text * * @return string */ private static function inflect($text) { $words = trim(preg_replace('/[\s_-]+/', ' ', preg_replace('/([a-z])([A-Z])/', '$1 $2', $text))); return implode(' ', array_map('ucfirst', explode(' ', $words))); } } getName(); } /** * Print the method, property or class modifiers. * * Technically this should be a trait. Can't wait for 5.4 :) * * @param \Reflector $reflector * * @return string Formatted modifiers */ private static function formatModifiers(\Reflector $reflector) { return implode(' ', array_map(function ($modifier) { return sprintf('%s', $modifier); }, \Reflection::getModifierNames($reflector->getModifiers()))); } /** * Format a class signature. * * @param \ReflectionClass $reflector * * @return string Formatted signature */ private static function formatClass(\ReflectionClass $reflector) { $chunks = array(); if ($modifiers = self::formatModifiers($reflector)) { $chunks[] = $modifiers; } if (version_compare(PHP_VERSION, '5.4', '>=') && $reflector->isTrait()) { $chunks[] = 'trait'; } else { $chunks[] = $reflector->isInterface() ? 'interface' : 'class'; } $chunks[] = sprintf('%s', self::formatName($reflector)); if ($parent = $reflector->getParentClass()) { $chunks[] = 'extends'; $chunks[] = sprintf('%s', $parent->getName()); } $interfaces = $reflector->getInterfaceNames(); if (!empty($interfaces)) { sort($interfaces); $chunks[] = 'implements'; $chunks[] = implode(', ', array_map(function ($name) { return sprintf('%s', $name); }, $interfaces)); } return implode(' ', $chunks); } /** * Format a constant signature. * * @param ReflectionConstant $reflector * * @return string Formatted signature */ private static function formatConstant(ReflectionConstant $reflector) { $value = $reflector->getValue(); $style = self::getTypeStyle($value); return sprintf( 'const %s = <%s>%s', self::formatName($reflector), $style, OutputFormatter::escape(Json::encode($value)), $style ); } /** * Helper for getting output style for a given value's type. * * @param mixed $value * * @return string */ private static function getTypeStyle($value) { if (is_int($value) || is_float($value)) { return 'number'; } elseif (is_string($value)) { return 'string'; } elseif (is_bool($value) || is_null($value)) { return 'bool'; } else { return 'strong'; } } /** * Format a property signature. * * @param \ReflectionProperty $reflector * * @return string Formatted signature */ private static function formatProperty(\ReflectionProperty $reflector) { return sprintf( '%s $%s', self::formatModifiers($reflector), $reflector->getName() ); } /** * Format a function signature. * * @param \ReflectionFunction $reflector * * @return string Formatted signature */ private static function formatFunction(\ReflectionFunctionAbstract $reflector) { return sprintf( 'function %s%s(%s)', $reflector->returnsReference() ? '&' : '', self::formatName($reflector), implode(', ', self::formatFunctionParams($reflector)) ); } /** * Format a method signature. * * @param \ReflectionMethod $reflector * * @return string Formatted signature */ private static function formatMethod(\ReflectionMethod $reflector) { return sprintf( '%s %s', self::formatModifiers($reflector), self::formatFunction($reflector) ); } /** * Print the function params. * * @param \ReflectionFunctionAbstract $reflector * * @return string */ private static function formatFunctionParams(\ReflectionFunctionAbstract $reflector) { $params = array(); foreach ($reflector->getParameters() as $param) { $hint = ''; try { if ($param->isArray()) { $hint = 'array '; } elseif ($class = $param->getClass()) { $hint = sprintf('%s ', $class->getName()); } } catch (\Exception $e) { // sometimes we just don't know... // bad class names, or autoloaded classes that haven't been loaded yet, or whathaveyou. // come to think of it, the only time I've seen this is with the intl extension. // Hax: we'll try to extract it :P $chunks = explode('$' . $param->getName(), (string) $param); $chunks = explode(' ', trim($chunks[0])); $guess = end($chunks); $hint = sprintf('%s ', $guess); } if ($param->isOptional()) { if (!$param->isDefaultValueAvailable()) { $value = 'unknown'; $typeStyle = 'urgent'; } else { $value = $param->getDefaultValue(); $typeStyle = self::getTypeStyle($value); $value = is_array($value) ? 'array()' : is_null($value) ? 'null' : var_export($value, true); } $default = sprintf(' = <%s>%s', $typeStyle, OutputFormatter::escape($value), $typeStyle); } else { $default = ''; } $params[] = sprintf( '%s%s$%s%s', $param->isPassedByReference() ? '&' : '', $hint, $param->getName(), $default ); } return $params; } } Shell::VERSION, 'PHP version' => PHP_VERSION, 'default includes' => $config->getDefaultIncludes(), 'require semicolons' => $config->requireSemicolons(), 'error logging level' => $config->errorLoggingLevel(), 'config file' => array( 'default config file' => $config->getConfigFile(), 'local config file' => $config->getLocalConfigFile(), 'PSYSH_CONFIG env' => getenv('PSYSH_CONFIG'), ), // 'config dir' => $config->getConfigDir(), // 'data dir' => $config->getDataDir(), // 'runtime dir' => $config->getRuntimeDir(), ); // Use an explicit, fresh update check here, rather than relying on whatever is in $config. $checker = new Checker(); $updates = array( 'update available' => !$checker->isLatest(), 'latest release version' => $checker->getLatest(), 'update check interval' => $config->getUpdateCheck(), 'update cache file' => $config->getUpdateCheckCacheFile(), ); if ($config->hasReadline()) { $info = readline_info(); $readline = array( 'readline available' => true, 'readline enabled' => $config->useReadline(), 'readline service' => get_class($config->getReadline()), 'readline library' => $info['library_version'], ); if ($info['readline_name'] !== '') { $readline['readline name'] = $info['readline_name']; } } else { $readline = array( 'readline available' => false, ); } $pcntl = array( 'pcntl available' => function_exists('pcntl_signal'), 'posix available' => function_exists('posix_getpid'), ); $history = array( 'history file' => $config->getHistoryFile(), 'history size' => $config->getHistorySize(), 'erase duplicates' => $config->getEraseDuplicates(), ); $docs = array( 'manual db file' => $config->getManualDbFile(), 'sqlite available' => true, ); try { if ($db = $config->getManualDb()) { if ($q = $db->query('SELECT * FROM meta;')) { $q->setFetchMode(\PDO::FETCH_KEY_PAIR); $meta = $q->fetchAll(); foreach ($meta as $key => $val) { switch ($key) { case 'built_at': $d = new \DateTime('@' . $val); $val = $d->format(\DateTime::RFC2822); break; } $key = 'db ' . str_replace('_', ' ', $key); $docs[$key] = $val; } } else { $docs['db schema'] = '0.1.0'; } } } catch (Exception\RuntimeException $e) { if ($e->getMessage() === 'SQLite PDO driver not found') { $docs['sqlite available'] = false; } else { throw $e; } } $autocomplete = array( 'tab completion enabled' => $config->getTabCompletion(), 'custom matchers' => array_map('get_class', $config->getTabCompletionMatchers()), ); return array_merge($core, compact('updates', 'pcntl', 'readline', 'history', 'docs', 'autocomplete')); } } if (!function_exists('Psy\bin')) { /** * `psysh` command line executable. * * @return Closure */ function bin() { return function () { $usageException = null; $input = new ArgvInput(); try { $input->bind(new InputDefinition(array( new InputOption('help', 'h', InputOption::VALUE_NONE), new InputOption('config', 'c', InputOption::VALUE_REQUIRED), new InputOption('version', 'v', InputOption::VALUE_NONE), new InputOption('cwd', null, InputOption::VALUE_REQUIRED), new InputOption('color', null, InputOption::VALUE_NONE), new InputOption('no-color', null, InputOption::VALUE_NONE), new InputArgument('include', InputArgument::IS_ARRAY), ))); } catch (\RuntimeException $e) { $usageException = $e; } $config = array(); // Handle --config if ($configFile = $input->getOption('config')) { $config['configFile'] = $configFile; } // Handle --color and --no-color if ($input->getOption('color') && $input->getOption('no-color')) { $usageException = new \RuntimeException('Using both "--color" and "--no-color" options is invalid.'); } elseif ($input->getOption('color')) { $config['colorMode'] = Configuration::COLOR_MODE_FORCED; } elseif ($input->getOption('no-color')) { $config['colorMode'] = Configuration::COLOR_MODE_DISABLED; } $shell = new Shell(new Configuration($config)); // Handle --help if ($usageException !== null || $input->getOption('help')) { if ($usageException !== null) { echo $usageException->getMessage() . PHP_EOL . PHP_EOL; } $version = $shell->getVersion(); $name = basename(reset($_SERVER['argv'])); echo <<getOption('version')) { echo $shell->getVersion() . PHP_EOL; exit(0); } // Pass additional arguments to Shell as 'includes' $shell->setIncludes($input->getArgument('include')); try { // And go! $shell->run(); } catch (Exception $e) { echo $e->getMessage() . PHP_EOL; // TODO: this triggers the "exited unexpectedly" logic in the // ForkingLoop, so we can't exit(1) after starting the shell... // fix this :) // exit(1); } }; } } getStream()); } /** * Close the current pager process. */ public function close() { // nothing to do here } } stream = $output->getStream(); $this->cmd = $cmd; } /** * Writes a message to the output. * * @param string $message A message to write to the output * @param bool $newline Whether to add a newline or not * * @throws \RuntimeException When unable to write output (should never happen) */ public function doWrite($message, $newline) { $pipe = $this->getPipe(); if (false === @fwrite($pipe, $message . ($newline ? PHP_EOL : ''))) { // @codeCoverageIgnoreStart // should never happen throw new \RuntimeException('Unable to write output.'); // @codeCoverageIgnoreEnd } fflush($pipe); } /** * Close the current pager process. */ public function close() { if (isset($this->pipe)) { fclose($this->pipe); } if (isset($this->proc)) { $exit = proc_close($this->proc); if ($exit !== 0) { throw new \RuntimeException('Error closing output stream'); } } unset($this->pipe, $this->proc); } /** * Get a pipe for paging output. * * If no active pager process exists, fork one and return its input pipe. */ private function getPipe() { if (!isset($this->pipe) || !isset($this->proc)) { $desc = array(array('pipe', 'r'), $this->stream, fopen('php://stderr', 'w')); $this->proc = proc_open($this->cmd, $desc, $pipes); if (!is_resource($this->proc)) { throw new \RuntimeException('Error opening output stream'); } $this->pipe = $pipes[0]; } return $this->pipe; } } initFormatters(); if ($pager === null) { $this->pager = new PassthruPager($this); } elseif (is_string($pager)) { $this->pager = new ProcOutputPager($this, $pager); } elseif ($pager instanceof OutputPager) { $this->pager = $pager; } else { throw new \InvalidArgumentException('Unexpected pager parameter: ' . $pager); } } /** * Page multiple lines of output. * * The output pager is started * * If $messages is callable, it will be called, passing this output instance * for rendering. Otherwise, all passed $messages are paged to output. * * Upon completion, the output pager is flushed. * * @param string|array|Closure $messages A string, array of strings or a callback * @param int $type (default: 0) */ public function page($messages, $type = 0) { if (is_string($messages)) { $messages = (array) $messages; } if (!is_array($messages) && !is_callable($messages)) { throw new \InvalidArgumentException('Paged output requires a string, array or callback.'); } $this->startPaging(); if (is_callable($messages)) { $messages($this); } else { $this->write($messages, true, $type); } $this->stopPaging(); } /** * Start sending output to the output pager. */ public function startPaging() { $this->paging++; } /** * Stop paging output and flush the output pager. */ public function stopPaging() { $this->paging--; $this->closePager(); } /** * Writes a message to the output. * * Optionally, pass `$type | self::NUMBER_LINES` as the $type parameter to * number the lines of output. * * @throws \InvalidArgumentException When unknown output type is given * * @param string|array $messages The message as an array of lines or a single string * @param bool $newline Whether to add a newline or not * @param int $type The type of output */ public function write($messages, $newline = false, $type = 0) { if ($this->getVerbosity() === self::VERBOSITY_QUIET) { return; } $messages = (array) $messages; if ($type & self::NUMBER_LINES) { $pad = strlen((string) count($messages)); $template = $this->isDecorated() ? ": %s" : "%{$pad}s: %s"; if ($type & self::OUTPUT_RAW) { $messages = array_map(array('Symfony\Component\Console\Formatter\OutputFormatter', 'escape'), $messages); } foreach ($messages as $i => $line) { $messages[$i] = sprintf($template, $i, $line); } // clean this up for super. $type = $type & ~self::NUMBER_LINES & ~self::OUTPUT_RAW; } parent::write($messages, $newline, $type); } /** * Writes a message to the output. * * Handles paged output, or writes directly to the output stream. * * @param string $message A message to write to the output * @param bool $newline Whether to add a newline or not */ public function doWrite($message, $newline) { if ($this->paging > 0) { $this->pager->doWrite($message, $newline); } else { parent::doWrite($message, $newline); } } /** * Flush and close the output pager. */ private function closePager() { if ($this->paging <= 0) { $this->pager->close(); } } /** * Initialize output formatter styles. */ private function initFormatters() { $formatter = $this->getFormatter(); $formatter->setStyle('warning', new OutputFormatterStyle('black', 'yellow')); $formatter->setStyle('error', new OutputFormatterStyle('black', 'red', array('bold'))); $formatter->setStyle('aside', new OutputFormatterStyle('blue')); $formatter->setStyle('strong', new OutputFormatterStyle(null, null, array('bold'))); $formatter->setStyle('return', new OutputFormatterStyle('cyan')); $formatter->setStyle('urgent', new OutputFormatterStyle('red')); $formatter->setStyle('hidden', new OutputFormatterStyle('black')); // Visibility $formatter->setStyle('public', new OutputFormatterStyle(null, null, array('bold'))); $formatter->setStyle('protected', new OutputFormatterStyle('yellow')); $formatter->setStyle('private', new OutputFormatterStyle('red')); $formatter->setStyle('global', new OutputFormatterStyle('cyan', null, array('bold'))); $formatter->setStyle('const', new OutputFormatterStyle('cyan')); $formatter->setStyle('class', new OutputFormatterStyle('blue', null, array('underscore'))); $formatter->setStyle('function', new OutputFormatterStyle(null)); $formatter->setStyle('default', new OutputFormatterStyle(null)); // Types $formatter->setStyle('number', new OutputFormatterStyle('magenta')); $formatter->setStyle('string', new OutputFormatterStyle('green')); $formatter->setStyle('bool', new OutputFormatterStyle('cyan')); $formatter->setStyle('keyword', new OutputFormatterStyle('yellow')); $formatter->setStyle('comment', new OutputFormatterStyle('blue')); $formatter->setStyle('object', new OutputFormatterStyle('blue')); $formatter->setStyle('resource', new OutputFormatterStyle('yellow')); } } = 2.0 — does. * * @return bool */ public function hasKindsSupport() { return class_exists('PhpParser\ParserFactory'); } /** * Default kind (if supported, based on current interpreter's version). * * @return string|null */ public function getDefaultKind() { if ($this->hasKindsSupport()) { return version_compare(PHP_VERSION, '7.0', '>=') ? static::ONLY_PHP7 : static::ONLY_PHP5; } } /** * New parser instance with given kind. * * @param string|null $kind One of class constants (only for PHP parser 2.0 and above) * * @return Parser */ public function createParser($kind = null) { if ($this->hasKindsSupport()) { $originalFactory = new OriginalParserFactory(); $kind = $kind ?: $this->getDefaultKind(); if (!in_array($kind, static::getPossibleKinds())) { throw new \InvalidArgumentException('Unknown parser kind'); } $parser = $originalFactory->create(constant('PhpParser\ParserFactory::' . $kind)); } else { if ($kind !== null) { throw new \InvalidArgumentException('Install PHP Parser v2.x to specify parser kind'); } $parser = new Parser(new Lexer()); } return $parser; } } historyFile = $historyFile; $this->historySize = $historySize; $this->eraseDups = $eraseDups; } /** * {@inheritdoc} */ public function addHistory($line) { if ($res = readline_add_history($line)) { $this->writeHistory(); } return $res; } /** * {@inheritdoc} */ public function clearHistory() { if ($res = readline_clear_history()) { $this->writeHistory(); } return $res; } /** * {@inheritdoc} */ public function listHistory() { return readline_list_history(); } /** * {@inheritdoc} */ public function readHistory() { // Workaround PHP bug #69054 // // If open_basedir is set, readline_read_history() segfaults. This will be fixed in 5.6.7: // // https://github.com/php/php-src/blob/423a057023ef3c00d2ffc16a6b43ba01d0f71796/NEWS#L19-L21 // // TODO: add a PHP version check after next point release if (!ini_get('open_basedir')) { readline_read_history(); } readline_clear_history(); return readline_read_history($this->historyFile); } /** * {@inheritdoc} */ public function readline($prompt = null) { return readline($prompt); } /** * {@inheritdoc} */ public function redisplay() { readline_redisplay(); } /** * {@inheritdoc} */ public function writeHistory() { // We have to write history first, since it is used // by Libedit to list history $res = readline_write_history($this->historyFile); if (!$res || !$this->eraseDups && !$this->historySize > 0) { return $res; } $hist = $this->listHistory(); if (!$hist) { return true; } if ($this->eraseDups) { // flip-flip technique: removes duplicates, latest entries win. $hist = array_flip(array_flip($hist)); // sort on keys to get the order back ksort($hist); } if ($this->historySize > 0) { $histsize = count($hist); if ($histsize > $this->historySize) { $hist = array_slice($hist, $histsize - $this->historySize); } } readline_clear_history(); foreach ($hist as $line) { readline_add_history($line); } return readline_write_history($this->historyFile); } } hoaReadline = new HoaReadline(); } /** * {@inheritdoc} */ public function addHistory($line) { $this->hoaReadline->addHistory($line); return true; } /** * {@inheritdoc} */ public function clearHistory() { $this->hoaReadline->clearHistory(); return true; } /** * {@inheritdoc} */ public function listHistory() { $i = 0; $list = array(); while (($item = $this->hoaReadline->getHistory($i++)) !== null) { $list[] = $item; } return $list; } /** * {@inheritdoc} */ public function readHistory() { return true; } /** * {@inheritdoc} * * @throws BreakException if user hits Ctrl+D * * @return string */ public function readline($prompt = null) { return $this->hoaReadline->readLine($prompt); } /** * {@inheritdoc} */ public function redisplay() { // noop } /** * {@inheritdoc} */ public function writeHistory() { return true; } } historyFile); if (!$history) { return array(); } // libedit doesn't seem to support non-unix line separators. $history = explode("\n", $history); // shift the history signature, ensure it's valid if (array_shift($history) !== '_HiStOrY_V2_') { return array(); } // decode the line $history = array_map(array($this, 'parseHistoryLine'), $history); // filter empty lines & comments return array_values(array_filter($history)); } /** * From GNUReadline (readline/histfile.c & readline/histexpand.c): * lines starting with "\0" are comments or timestamps; * if "\0" is found in an entry, * everything from it until the next line is a comment. * * @param string $line The history line to parse * * @return string | null */ protected function parseHistoryLine($line) { // empty line, comment or timestamp if (!$line || $line[0] === "\0") { return; } // if "\0" is found in an entry, then // everything from it until the end of line is a comment. if (($pos = strpos($line, "\0")) !== false) { $line = substr($line, 0, $pos); } return ($line !== '') ? Str::unvis($line) : null; } } history = array(); $this->historySize = $historySize; $this->eraseDups = $eraseDups; } /** * {@inheritdoc} */ public function addHistory($line) { if ($this->eraseDups) { if (($key = array_search($line, $this->history)) !== false) { unset($this->history[$key]); } } $this->history[] = $line; if ($this->historySize > 0) { $histsize = count($this->history); if ($histsize > $this->historySize) { $this->history = array_slice($this->history, $histsize - $this->historySize); } } $this->history = array_values($this->history); return true; } /** * {@inheritdoc} */ public function clearHistory() { $this->history = array(); return true; } /** * {@inheritdoc} */ public function listHistory() { return $this->history; } /** * {@inheritdoc} */ public function readHistory() { return true; } /** * {@inheritdoc} * * @throws BreakException if user hits Ctrl+D * * @return string */ public function readline($prompt = null) { echo $prompt; return rtrim(fgets($this->getStdin(), 1024)); } /** * {@inheritdoc} */ public function redisplay() { // noop } /** * {@inheritdoc} */ public function writeHistory() { return true; } /** * Get a STDIN file handle. * * @throws BreakException if user hits Ctrl+D * * @return resource */ private function getStdin() { if (!isset($this->stdin)) { $this->stdin = fopen('php://stdin', 'r'); } if (feof($this->stdin)) { throw new BreakException('Ctrl+D'); } return $this->stdin; } } class = $class; $this->name = $name; $constants = $class->getConstants(); if (!array_key_exists($name, $constants)) { throw new \InvalidArgumentException('Unknown constant: ' . $name); } $this->value = $constants[$name]; } /** * Gets the declaring class. * * @return string */ public function getDeclaringClass() { return $this->class; } /** * Gets the constant name. * * @return string */ public function getName() { return $this->name; } /** * Gets the value of the constant. * * @return mixed */ public function getValue() { return $this->value; } /** * Gets the constant's file name. * * Currently returns null, because if it returns a file name the signature * formatter will barf. */ public function getFileName() { return; // return $this->class->getFileName(); } /** * Get the code start line. * * @throws \RuntimeException */ public function getStartLine() { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * Get the code end line. * * @throws \RuntimeException */ public function getEndLine() { return $this->getStartLine(); } /** * Get the constant's docblock. * * @return false */ public function getDocComment() { return false; } /** * Export the constant? I don't think this is possible. * * @throws \RuntimeException */ public static function export() { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * To string. * * @return string */ public function __toString() { return $this->getName(); } } array( 'var' => array(), '...' => array( 'isOptional' => true, 'defaultValue' => null, ), ), 'unset' => array( 'var' => array(), '...' => array( 'isOptional' => true, 'defaultValue' => null, ), ), 'empty' => array( 'var' => array(), ), 'echo' => array( 'arg1' => array(), '...' => array( 'isOptional' => true, 'defaultValue' => null, ), ), 'print' => array( 'arg' => array(), ), 'die' => array( 'status' => array( 'isOptional' => true, 'defaultValue' => 0, ), ), 'exit' => array( 'status' => array( 'isOptional' => true, 'defaultValue' => 0, ), ), ); /** * Construct a ReflectionLanguageConstruct object. * * @param string $name */ public function __construct($keyword) { if (self::isLanguageConstruct($keyword)) { throw new \InvalidArgumentException('Unknown language construct: ' . $keyword); } $this->keyword = $keyword; } /** * This can't (and shouldn't) do anything :). * * @throws \RuntimeException */ public static function export($name) { throw new \RuntimeException('Not yet implemented because it\'s unclear what I should do here :)'); } /** * Get language construct name. * * @return string */ public function getName() { return $this->keyword; } /** * None of these return references. * * @return bool */ public function returnsReference() { return false; } /** * Get language construct params. * * @return */ public function getParameters() { $params = array(); foreach (self::$languageConstructs[$this->keyword] as $parameter => $opts) { array_push($params, new ReflectionLanguageConstructParameter($this->keyword, $parameter, $opts)); } return $params; } /** * To string. * * @return string */ public function __toString() { return $this->getName(); } /** * Check whether keyword is a (known) language construct. * * @param $keyword * * @return bool */ public static function isLanguageConstruct($keyword) { return array_key_exists($keyword, self::$languageConstructs); } } function = $function; $this->parameter = $parameter; $this->opts = $opts; } /** * No class here. */ public function getClass() { return; } /** * Is the param an array? * * @return bool */ public function isArray() { return array_key_exists('isArray', $this->opts) && $this->opts['isArray']; } /** * Get param default value. * * @return mixed */ public function getDefaultValue() { if ($this->isDefaultValueAvailable()) { return $this->opts['defaultValue']; } } /** * Get param name. * * @return string */ public function getName() { return $this->parameter; } /** * Is the param optional? * * @return bool */ public function isOptional() { return array_key_exists('isOptional', $this->opts) && $this->opts['isOptional']; } /** * Does the param have a default value? * * @return bool */ public function isDefaultValueAvailable() { return array_key_exists('defaultValue', $this->opts); } /** * Is the param passed by reference? * * (I don't think this is true for anything we need to fake a param for) * * @return bool */ public function isPassedByReference() { return array_key_exists('isPassedByReference', $this->opts) && $this->opts['isPassedByReference']; } } run(); * * @author Justin Hileman */ class Shell extends Application { const VERSION = 'v0.8.0'; const PROMPT = '>>> '; const BUFF_PROMPT = '... '; const REPLAY = '--> '; const RETVAL = '=> '; private $config; private $cleaner; private $output; private $readline; private $inputBuffer; private $code; private $codeBuffer; private $codeBufferOpen; private $context; private $includes; private $loop; private $outputWantsNewline = false; private $completion; private $tabCompletionMatchers = array(); /** * Create a new Psy Shell. * * @param Configuration $config (default: null) */ public function __construct(Configuration $config = null) { $this->config = $config ?: new Configuration(); $this->cleaner = $this->config->getCodeCleaner(); $this->loop = $this->config->getLoop(); $this->context = new Context(); $this->includes = array(); $this->readline = $this->config->getReadline(); parent::__construct('Psy Shell', self::VERSION); $this->config->setShell($this); } /** * Check whether the first thing in a backtrace is an include call. * * This is used by the psysh bin to decide whether to start a shell on boot, * or to simply autoload the library. */ public static function isIncluded(array $trace) { return isset($trace[0]['function']) && in_array($trace[0]['function'], array('require', 'include', 'require_once', 'include_once')); } /** * Invoke a Psy Shell from the current context. * * For example: * * foreach ($items as $item) { * \Psy\Shell::debug(get_defined_vars()); * } * * If you would like your shell interaction to affect the state of the * current context, you can extract() the values returned from this call: * * foreach ($items as $item) { * extract(\Psy\Shell::debug(get_defined_vars())); * var_dump($item); // will be whatever you set $item to in Psy Shell * } * * Optionally, supply an object as the `$bind` parameter. This determines * the value `$this` will have in the shell, and sets up class scope so that * private and protected members are accessible: * * class Foo { * function bar() { * \Psy\Shell::debug(get_defined_vars(), $this); * } * } * * This only really works in PHP 5.4+ and HHVM 3.5+, so upgrade already. * * @param array $vars Scope variables from the calling context (default: array()) * @param object $bind Bound object ($this) value for the shell * * @return array Scope variables from the debugger session */ public static function debug(array $vars = array(), $bind = null) { echo PHP_EOL; if ($bind !== null) { $vars['this'] = $bind; } $sh = new \Psy\Shell(); $sh->setScopeVariables($vars); $sh->run(); return $sh->getScopeVariables(); } /** * Adds a command object. * * {@inheritdoc} * * @param BaseCommand $command A Symfony Console Command object * * @return BaseCommand The registered command */ public function add(BaseCommand $command) { if ($ret = parent::add($command)) { if ($ret instanceof ContextAware) { $ret->setContext($this->context); } if ($ret instanceof PresenterAware) { $ret->setPresenter($this->config->getPresenter()); } } return $ret; } /** * 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.'), )); } /** * Gets the default commands that should always be available. * * @return array An array of default Command instances */ protected function getDefaultCommands() { $hist = new Command\HistoryCommand(); $hist->setReadline($this->readline); return array( new Command\HelpCommand(), new Command\ListCommand(), new Command\DumpCommand(), new Command\DocCommand(), new Command\ShowCommand($this->config->colorMode()), new Command\WtfCommand(), new Command\WhereamiCommand($this->config->colorMode()), new Command\ThrowUpCommand(), new Command\TraceCommand(), new Command\BufferCommand(), new Command\ClearCommand(), // new Command\PsyVersionCommand(), $hist, new Command\ExitCommand(), ); } /** * @return array */ protected function getTabCompletionMatchers() { if (empty($this->tabCompletionMatchers)) { $this->tabCompletionMatchers = array( new Matcher\CommandsMatcher($this->all()), new Matcher\KeywordsMatcher(), new Matcher\VariablesMatcher(), new Matcher\ConstantsMatcher(), new Matcher\FunctionsMatcher(), new Matcher\ClassNamesMatcher(), new Matcher\ClassMethodsMatcher(), new Matcher\ClassAttributesMatcher(), new Matcher\ObjectMethodsMatcher(), new Matcher\ObjectAttributesMatcher(), ); } return $this->tabCompletionMatchers; } /** * @param array $matchers */ public function addTabCompletionMatchers(array $matchers) { $this->tabCompletionMatchers = array_merge($matchers, $this->getTabCompletionMatchers()); } /** * Set the Shell output. * * @param OutputInterface $output */ public function setOutput(OutputInterface $output) { $this->output = $output; } /** * Runs the current application. * * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * * @return int 0 if everything went fine, or an error code */ public function run(InputInterface $input = null, OutputInterface $output = null) { $this->initializeTabCompletion(); if ($input === null && !isset($_SERVER['argv'])) { $input = new ArgvInput(array()); } if ($output === null) { $output = $this->config->getOutput(); } try { return parent::run($input, $output); } catch (\Exception $e) { $this->writeException($e); } } /** * Runs the current application. * * @throws Exception if thrown via the `throw-up` command * * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * * @return int 0 if everything went fine, or an error code */ public function doRun(InputInterface $input, OutputInterface $output) { $this->setOutput($output); $this->resetCodeBuffer(); $this->setAutoExit(false); $this->setCatchExceptions(false); $this->readline->readHistory(); // if ($this->config->useReadline()) { // readline_completion_function(array($this, 'autocomplete')); // } $this->output->writeln($this->getHeader()); $this->writeVersionInfo(); try { $this->loop->run($this); } catch (ThrowUpException $e) { throw $e->getPrevious(); } } /** * Read user input. * * This will continue fetching user input until the code buffer contains * valid code. * * @throws BreakException if user hits Ctrl+D */ public function getInput() { $this->codeBufferOpen = false; do { // reset output verbosity (in case it was altered by a subcommand) $this->output->setVerbosity(ShellOutput::VERBOSITY_VERBOSE); $input = $this->readline(); /* * Handle Ctrl+D. It behaves differently in different cases: * * 1) In an expression, like a function or "if" block, clear the input buffer * 2) At top-level session, behave like the exit command */ if ($input === false) { $this->output->writeln(''); if ($this->hasCode()) { $this->resetCodeBuffer(); } else { throw new BreakException('Ctrl+D'); } } // handle empty input if (trim($input) === '') { continue; } if ($this->hasCommand($input)) { $this->readline->addHistory($input); $this->runCommand($input); continue; } $this->addCode($input); } while (!$this->hasValidCode()); } /** * Pass the beforeLoop callback through to the Loop instance. * * @see Loop::beforeLoop */ public function beforeLoop() { $this->loop->beforeLoop(); } /** * Pass the afterLoop callback through to the Loop instance. * * @see Loop::afterLoop */ public function afterLoop() { $this->loop->afterLoop(); } /** * Set the variables currently in scope. * * @param array $vars */ public function setScopeVariables(array $vars) { $this->context->setAll($vars); } /** * Return the set of variables currently in scope. * * @return array Associative array of scope variables */ public function getScopeVariables() { return $this->context->getAll(); } /** * Get the set of variable names currently in scope. * * @return array Array of variable names */ public function getScopeVariableNames() { return array_keys($this->context->getAll()); } /** * Get a scope variable value by name. * * @param string $name * * @return mixed */ public function getScopeVariable($name) { return $this->context->get($name); } /** * Add includes, to be parsed and executed before running the interactive shell. * * @param array $includes */ public function setIncludes(array $includes = array()) { $this->includes = $includes; } /** * Get PHP files to be parsed and executed before running the interactive shell. * * @return array */ public function getIncludes() { return array_merge($this->config->getDefaultIncludes(), $this->includes); } /** * Check whether this shell's code buffer contains code. * * @return bool True if the code buffer contains code */ public function hasCode() { return !empty($this->codeBuffer); } /** * Check whether the code in this shell's code buffer is valid. * * If the code is valid, the code buffer should be flushed and evaluated. * * @return bool True if the code buffer content is valid */ protected function hasValidCode() { return !$this->codeBufferOpen && $this->code !== false; } /** * Add code to the code buffer. * * @param string $code */ public function addCode($code) { try { // Code lines ending in \ keep the buffer open if (substr(rtrim($code), -1) === '\\') { $this->codeBufferOpen = true; $code = substr(rtrim($code), 0, -1); } else { $this->codeBufferOpen = false; } $this->codeBuffer[] = $code; $this->code = $this->cleaner->clean($this->codeBuffer, $this->config->requireSemicolons()); } catch (\Exception $e) { // Add failed code blocks to the readline history. $this->readline->addHistory(implode("\n", $this->codeBuffer)); throw $e; } } /** * Get the current code buffer. * * This is useful for commands which manipulate the buffer. * * @return array */ public function getCodeBuffer() { return $this->codeBuffer; } /** * Run a Psy Shell command given the user input. * * @throws InvalidArgumentException if the input is not a valid command * * @param string $input User input string * * @return mixed Who knows? */ protected function runCommand($input) { $command = $this->getCommand($input); if (empty($command)) { throw new \InvalidArgumentException('Command not found: ' . $input); } $input = new StringInput(str_replace('\\', '\\\\', rtrim($input, " \t\n\r\0\x0B;"))); if ($input->hasParameterOption(array('--help', '-h'))) { $helpCommand = $this->get('help'); $helpCommand->setCommand($command); return $helpCommand->run($input, $this->output); } return $command->run($input, $this->output); } /** * Reset the current code buffer. * * This should be run after evaluating user input, catching exceptions, or * on demand by commands such as BufferCommand. */ public function resetCodeBuffer() { $this->codeBuffer = array(); $this->code = false; } /** * Inject input into the input buffer. * * This is useful for commands which want to replay history. * * @param string|array $input */ public function addInput($input) { foreach ((array) $input as $line) { $this->inputBuffer[] = $line; } } /** * Flush the current (valid) code buffer. * * If the code buffer is valid, resets the code buffer and returns the * current code. * * @return string PHP code buffer contents */ public function flushCode() { if ($this->hasValidCode()) { $this->readline->addHistory(implode("\n", $this->codeBuffer)); $code = $this->code; $this->resetCodeBuffer(); return $code; } } /** * Get the current evaluation scope namespace. * * @see CodeCleaner::getNamespace * * @return string Current code namespace */ public function getNamespace() { if ($namespace = $this->cleaner->getNamespace()) { return implode('\\', $namespace); } } /** * Write a string to stdout. * * This is used by the shell loop for rendering output from evaluated code. * * @param string $out * @param int $phase Output buffering phase */ public function writeStdout($out, $phase = PHP_OUTPUT_HANDLER_END) { $isCleaning = false; if (version_compare(PHP_VERSION, '5.4', '>=')) { $isCleaning = $phase & PHP_OUTPUT_HANDLER_CLEAN; } // Incremental flush if ($out !== '' && !$isCleaning) { $this->output->write($out, false, ShellOutput::OUTPUT_RAW); $this->outputWantsNewline = (substr($out, -1) !== "\n"); } // Output buffering is done! if ($this->outputWantsNewline && $phase & PHP_OUTPUT_HANDLER_END) { $this->output->writeln(sprintf('', $this->config->useUnicode() ? '⏎' : '\\n')); $this->outputWantsNewline = false; } } /** * Write a return value to stdout. * * The return value is formatted or pretty-printed, and rendered in a * visibly distinct manner (in this case, as cyan). * * @see self::presentValue * * @param mixed $ret */ public function writeReturnValue($ret) { $this->context->setReturnValue($ret); $ret = $this->presentValue($ret); $indent = str_repeat(' ', strlen(self::RETVAL)); $this->output->writeln(self::RETVAL . str_replace(PHP_EOL, PHP_EOL . $indent, $ret)); } /** * Renders a caught Exception. * * Exceptions are formatted according to severity. ErrorExceptions which were * warnings or Strict errors aren't rendered as harshly as real errors. * * Stores $e as the last Exception in the Shell Context. * * @param \Exception $e An exception instance * @param OutputInterface $output An OutputInterface instance */ public function writeException(\Exception $e) { $this->context->setLastException($e); $message = $e->getMessage(); if (!$e instanceof PsyException) { $message = sprintf('%s with message \'%s\'', get_class($e), $message); } $severity = ($e instanceof \ErrorException) ? $this->getSeverity($e) : 'error'; $this->output->writeln(sprintf('<%s>%s', $severity, OutputFormatter::escape($message), $severity)); $this->resetCodeBuffer(); } /** * Helper for getting an output style for the given ErrorException's level. * * @param \ErrorException $e * * @return string */ protected function getSeverity(\ErrorException $e) { $severity = $e->getSeverity(); if ($severity & error_reporting()) { switch ($severity) { case E_WARNING: case E_NOTICE: case E_CORE_WARNING: case E_COMPILE_WARNING: case E_USER_WARNING: case E_USER_NOTICE: case E_STRICT: return 'warning'; default: return 'error'; } } else { // Since this is below the user's reporting threshold, it's always going to be a warning. return 'warning'; } } /** * Helper for throwing an ErrorException. * * This allows us to: * * set_error_handler(array($psysh, 'handleError')); * * Unlike ErrorException::throwException, this error handler respects the * current error_reporting level; i.e. it logs warnings and notices, but * doesn't throw an exception unless it's above the current error_reporting * threshold. This should probably only be used in the inner execution loop * of the shell, as most of the time a thrown exception is much more useful. * * If the error type matches the `errorLoggingLevel` config, it will be * logged as well, regardless of the `error_reporting` level. * * @see \Psy\Exception\ErrorException::throwException * @see \Psy\Shell::writeException * * @throws \Psy\Exception\ErrorException depending on the current error_reporting level * * @param int $errno Error type * @param string $errstr Message * @param string $errfile Filename * @param int $errline Line number */ public function handleError($errno, $errstr, $errfile, $errline) { if ($errno & error_reporting()) { ErrorException::throwException($errno, $errstr, $errfile, $errline); } elseif ($errno & $this->config->errorLoggingLevel()) { // log it and continue... $this->writeException(new ErrorException($errstr, 0, $errno, $errfile, $errline)); } } /** * Format a value for display. * * @see Presenter::present * * @param mixed $val * * @return string Formatted value */ protected function presentValue($val) { return $this->config->getPresenter()->present($val); } /** * Get a command (if one exists) for the current input string. * * @param string $input * * @return null|Command */ protected function getCommand($input) { $input = new StringInput($input); if ($name = $input->getFirstArgument()) { return $this->get($name); } } /** * Check whether a command is set for the current input string. * * @param string $input * * @return bool True if the shell has a command for the given input */ protected function hasCommand($input) { $input = new StringInput($input); if ($name = $input->getFirstArgument()) { return $this->has($name); } return false; } /** * Get the current input prompt. * * @return string */ protected function getPrompt() { return $this->hasCode() ? self::BUFF_PROMPT : self::PROMPT; } /** * Read a line of user input. * * This will return a line from the input buffer (if any exist). Otherwise, * it will ask the user for input. * * If readline is enabled, this delegates to readline. Otherwise, it's an * ugly `fgets` call. * * @return string One line of user input */ protected function readline() { if (!empty($this->inputBuffer)) { $line = array_shift($this->inputBuffer); $this->output->writeln(sprintf('', self::REPLAY, OutputFormatter::escape($line))); return $line; } return $this->readline->readline($this->getPrompt()); } /** * Get the shell output header. * * @return string */ protected function getHeader() { return sprintf('', $this->getVersion()); } /** * Get the current version of Psy Shell. * * @return string */ public function getVersion() { $separator = $this->config->useUnicode() ? '—' : '-'; return sprintf('Psy Shell %s (PHP %s %s %s)', self::VERSION, phpversion(), $separator, php_sapi_name()); } /** * Get a PHP manual database instance. * * @return PDO|null */ public function getManualDb() { return $this->config->getManualDb(); } /** * Autocomplete variable names. * * This is used by `readline` for tab completion. * * @param string $text * * @return mixed Array possible completions for the given input, if any */ protected function autocomplete($text) { $info = readline_info(); // $line = substr($info['line_buffer'], 0, $info['end']); // Check whether there's a command for this // $words = explode(' ', $line); // $firstWord = reset($words); // check whether this is a variable... $firstChar = substr($info['line_buffer'], max(0, $info['end'] - strlen($text) - 1), 1); if ($firstChar === '$') { return $this->getScopeVariableNames(); } } /** * Initialize tab completion matchers. * * If tab completion is enabled this adds tab completion matchers to the * auto completer and sets context if needed. */ protected function initializeTabCompletion() { // auto completer needs shell to be linked to configuration because of the context aware matchers if ($this->config->getTabCompletion()) { $this->completion = $this->config->getAutoCompleter(); $this->addTabCompletionMatchers($this->config->getTabCompletionMatchers()); foreach ($this->getTabCompletionMatchers() as $matcher) { if ($matcher instanceof ContextAware) { $matcher->setContext($this->context); } $this->completion->addMatcher($matcher); } $this->completion->activate(); } } /** * @todo Implement self-update * @todo Implement prompt to start update * * @return void|string */ protected function writeVersionInfo() { if (PHP_SAPI !== 'cli') { return; } try { $client = $this->config->getChecker(); if (!$client->isLatest()) { $this->output->writeln(sprintf('New version is available (current: %s, latest: %s)',self::VERSION, $client->getLatest())); } } catch (\InvalidArgumentException $e) { $this->output->writeln($e->getMessage()); } } } */ class AutoCompleter { /** @var Matcher\AbstractMatcher[] */ protected $matchers; /** * Register a tab completion Matcher. * * @param AbstractMatcher $matcher */ public function addMatcher(AbstractMatcher $matcher) { $this->matchers[] = $matcher; } /** * Activate readline tab completion. */ public function activate() { readline_completion_function(array(&$this, 'callback')); } /** * Handle readline completion. * * @param string $input Readline current word * @param int $index Current word index * @param array $info readline_info() data * * @return array */ public function processCallback($input, $index, $info = array()) { $line = substr($info['line_buffer'], 0, $info['end']); $tokens = token_get_all('matchers as $matcher) { if ($matcher->hasMatched($tokens)) { $matches = array_merge($matcher->getMatches($tokens), $matches); } } $matches = array_unique($matches); return !empty($matches) ? $matches : array(''); } /** * The readline_completion_function callback handler. * * @see processCallback * * @param $input * @param $index * * @return array */ public function callback($input, $index) { return $this->processCallback($input, $index, readline_info()); } /** * Remove readline callback handler on destruct. */ public function __destruct() { // PHP didn't implement the whole readline API when they first switched // to libedit. And they still haven't. // // So this is a thing to make PsySH work on 5.3.x: if (function_exists('readline_callback_handler_remove')) { readline_callback_handler_remove(); } } } */ abstract class AbstractContextAwareMatcher extends AbstractMatcher implements ContextAware { /** * Context instance (for ContextAware interface). * * @var Context */ protected $context; /** * ContextAware interface. * * @param Context $context */ public function setContext(Context $context) { $this->context = $context; } /** * Get a Context variable by name. * * @param $var Variable name * * @return mixed */ protected function getVariable($var) { return $this->context->get($var); } /** * Get all variables in the current Context. * * @return array */ protected function getVariables() { return $this->context->getAll(); } } */ abstract class AbstractMatcher { /** Syntax types */ const CONSTANT_SYNTAX = '^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$'; const VAR_SYNTAX = '^\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$'; const MISC_OPERATORS = '+-*/^|&'; /** Token values */ const T_OPEN_TAG = 'T_OPEN_TAG'; const T_VARIABLE = 'T_VARIABLE'; const T_OBJECT_OPERATOR = 'T_OBJECT_OPERATOR'; const T_DOUBLE_COLON = 'T_DOUBLE_COLON'; const T_NEW = 'T_NEW'; const T_CLONE = 'T_CLONE'; const T_NS_SEPARATOR = 'T_NS_SEPARATOR'; const T_STRING = 'T_STRING'; const T_WHITESPACE = 'T_WHITESPACE'; const T_AND_EQUAL = 'T_AND_EQUAL'; const T_BOOLEAN_AND = 'T_BOOLEAN_AND'; const T_BOOLEAN_OR = 'T_BOOLEAN_OR'; const T_ENCAPSED_AND_WHITESPACE = 'T_ENCAPSED_AND_WHITESPACE'; const T_REQUIRE = 'T_REQUIRE'; const T_REQUIRE_ONCE = 'T_REQUIRE_ONCE'; const T_INCLUDE = 'T_INCLUDE'; const T_INCLUDE_ONCE = 'T_INCLUDE_ONCE'; /** * Check whether this matcher can provide completions for $tokens. * * @param array $tokens Tokenized readline input * * @return bool */ public function hasMatched(array $tokens) { return false; } /** * Get current readline input word. * * @param array $tokens Tokenized readline input (see token_get_all) * * @return string */ protected function getInput(array $tokens) { $var = ''; $firstToken = array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { $var = $firstToken[1]; } return $var; } /** * Get current namespace and class (if any) from readline input. * * @param array $tokens Tokenized readline input (see token_get_all) * * @return string */ protected function getNamespaceAndClass($tokens) { $class = ''; while (self::hasToken( array(self::T_NS_SEPARATOR, self::T_STRING), $token = array_pop($tokens) )) { $class = $token[1] . $class; } return $class; } /** * Provide tab completion matches for readline input. * * @param array $tokens information substracted with get_token_all * @param array $info readline_info object * * @return array The matches resulting from the query */ abstract public function getMatches(array $tokens, array $info = array()); /** * Check whether $word starts with $prefix. * * @param string $prefix * @param string $word * * @return bool */ public static function startsWith($prefix, $word) { return preg_match(sprintf('#^%s#', $prefix), $word); } /** * Check whether $token matches a given syntax pattern. * * @param mixed $token A PHP token (see token_get_all) * @param string $syntax A syntax pattern (default: variable pattern) * * @return bool */ public static function hasSyntax($token, $syntax = self::VAR_SYNTAX) { if (!is_array($token)) { return false; } $regexp = sprintf('#%s#', $syntax); return (bool) preg_match($regexp, $token[1]); } /** * Check whether $token type is $which. * * @param string $which A PHP token type * @param mixed $token A PHP token (see token_get_all) * * @return bool */ public static function tokenIs($token, $which) { if (!is_array($token)) { return false; } return token_name($token[0]) === $which; } /** * Check whether $token is an operator. * * @param mixed $token A PHP token (see token_get_all) * * @return bool */ public static function isOperator($token) { if (!is_string($token)) { return false; } return strpos(self::MISC_OPERATORS, $token) !== false; } /** * Check whether $token type is present in $coll. * * @param array $coll A list of token types * @param mixed $token A PHP token (see token_get_all) * * @return bool */ public static function hasToken(array $coll, $token) { if (!is_array($token)) { return false; } return in_array(token_name($token[0]), $coll); } } */ class ClassAttributesMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $input = $this->getInput($tokens); $firstToken = array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the nekudotayim operator array_pop($tokens); } $class = $this->getNamespaceAndClass($tokens); $reflection = new \ReflectionClass($class); $vars = array_merge( array_map( function ($var) { return '$' . $var; }, array_keys($reflection->getStaticProperties()) ), array_keys($reflection->getConstants()) ); return array_map( function ($name) use ($class) { return $class . '::' . $name; }, array_filter( $vars, function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ) ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); $prevToken = array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_DOUBLE_COLON) && self::tokenIs($token, self::T_STRING): case self::tokenIs($token, self::T_DOUBLE_COLON): return true; } return false; } } */ class ClassMethodsMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $input = $this->getInput($tokens); $firstToken = array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the nekudotayim operator array_pop($tokens); } $class = $this->getNamespaceAndClass($tokens); $reflection = new \ReflectionClass($class); $methods = $reflection->getMethods(\ReflectionMethod::IS_STATIC); $methods = array_map(function (\ReflectionMethod $method) { return $method->getName(); }, $methods); return array_map( function ($name) use ($class) { return $class . '::' . $name; }, array_filter($methods, function ($method) use ($input) { return AbstractMatcher::startsWith($input, $method); }) ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); $prevToken = array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_DOUBLE_COLON) && self::tokenIs($token, self::T_STRING): case self::tokenIs($token, self::T_DOUBLE_COLON): return true; } return false; } } */ class ClassNamesMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $class = $this->getNamespaceAndClass($tokens); if (strlen($class) > 0 && $class[0] === '\\') { $class = substr($class, 1, strlen($class)); } $quotedClass = preg_quote($class); return array_map( function ($className) use ($class) { // get the number of namespace separators $nsPos = substr_count($class, '\\'); $pieces = explode('\\', $className); //$methods = Mirror::get($class); return implode('\\', array_slice($pieces, $nsPos, count($pieces))); }, array_filter( get_declared_classes(), function ($className) use ($quotedClass) { return AbstractMatcher::startsWith($quotedClass, $className); } ) ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); $prevToken = array_pop($tokens); $blacklistedTokens = array( self::T_INCLUDE, self::T_INCLUDE_ONCE, self::T_REQUIRE, self::T_REQUIRE_ONCE, ); switch (true) { case self::hasToken(array($blacklistedTokens), $token): case self::hasToken(array($blacklistedTokens), $prevToken): case is_string($token) && $token === '$': return false; case self::hasToken(array(self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR), $prevToken): case self::hasToken(array(self::T_NEW, self::T_OPEN_TAG, self::T_NS_SEPARATOR), $token): case self::hasToken(array(self::T_OPEN_TAG, self::T_VARIABLE), $token): case self::isOperator($token): return true; } return false; } } */ class CommandsMatcher extends AbstractMatcher { /** @var string[] */ protected $commands = array(); /** * CommandsMatcher constructor. * * @param Command[] $commands */ public function __construct(array $commands) { $this->setCommands($commands); } /** * Set Commands for completion. * * @param Command[] $commands */ public function setCommands(array $commands) { $names = array(); foreach ($commands as $command) { $names = array_merge(array($command->getName()), $names); $names = array_merge($command->getAliases(), $names); } $this->commands = $names; } /** * Check whether a command $name is defined. * * @param string $name * * @return bool */ protected function isCommand($name) { return in_array($name, $this->commands); } /** * Check whether input matches a defined command. * * @param string $name * * @return bool */ protected function matchCommand($name) { foreach ($this->commands as $cmd) { if ($this->startsWith($name, $cmd)) { return true; } } return false; } /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $input = $this->getInput($tokens); return array_filter($this->commands, function ($command) use ($input) { return AbstractMatcher::startsWith($input, $command); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { /* $openTag */ array_shift($tokens); $command = array_shift($tokens); switch (true) { case self::tokenIs($command, self::T_STRING) && !$this->isCommand($command[1]) && $this->matchCommand($command[1]) && empty($tokens): return true; } return false; } } */ class ConstantsMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $const = $this->getInput($tokens); return array_filter(array_keys(get_defined_constants()), function ($constant) use ($const) { return AbstractMatcher::startsWith($const, $constant); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); $prevToken = array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_NEW): case self::tokenIs($prevToken, self::T_NS_SEPARATOR): return false; case self::hasToken(array(self::T_OPEN_TAG, self::T_STRING), $token): case self::isOperator($token): return true; } return false; } } */ class FunctionsMatcher extends AbstractMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $func = $this->getInput($tokens); $functions = get_defined_functions(); $allFunctions = array_merge($functions['user'], $functions['internal']); return array_filter($allFunctions, function ($function) use ($func) { return AbstractMatcher::startsWith($func, $function); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); $prevToken = array_pop($tokens); switch (true) { case self::tokenIs($prevToken, self::T_NEW): return false; case self::hasToken(array(self::T_OPEN_TAG, self::T_STRING), $token): case self::isOperator($token): return true; } return false; } } */ class KeywordsMatcher extends AbstractMatcher { protected $keywords = array( 'array', 'clone', 'declare', 'die', 'echo', 'empty', 'eval', 'exit', 'include', 'include_once', 'isset', 'list', 'print', 'require', 'require_once', 'unset', ); protected $mandatoryStartKeywords = array( 'die', 'echo', 'print', 'unset', ); /** * Get all (completable) PHP keywords. * * @return array */ public function getKeywords() { return $this->keywords; } /** * Check whether $keyword is a (completable) PHP keyword. * * @param string $keyword * * @return bool */ public function isKeyword($keyword) { return in_array($keyword, $this->keywords); } /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $input = $this->getInput($tokens); return array_filter($this->keywords, function ($keyword) use ($input) { return AbstractMatcher::startsWith($input, $keyword); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); $prevToken = array_pop($tokens); switch (true) { case self::hasToken(array(self::T_OPEN_TAG, self::T_VARIABLE), $token): // case is_string($token) && $token === '$': case self::hasToken(array(self::T_OPEN_TAG, self::T_VARIABLE), $prevToken) && self::tokenIs($token, self::T_STRING): case self::isOperator($token): return true; } return false; } } */ class MongoClientMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $input = $this->getInput($tokens); $firstToken = array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator array_pop($tokens); } $objectToken = array_pop($tokens); $objectName = str_replace('$', '', $objectToken[1]); $object = $this->getVariable($objectName); if (!$object instanceof \MongoClient) { return array(); } $list = $object->listDBs(); return array_filter( array_map(function ($info) { return $info['name']; }, $list['databases']), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); $prevToken = array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } */ class MongoDatabaseMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $input = $this->getInput($tokens); $firstToken = array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator array_pop($tokens); } $objectToken = array_pop($tokens); $objectName = str_replace('$', '', $objectToken[1]); $object = $this->getVariable($objectName); if (!$object instanceof \MongoDB) { return array(); } return array_filter( $object->getCollectionNames(), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); $prevToken = array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } */ class ObjectAttributesMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $input = $this->getInput($tokens); $firstToken = array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator array_pop($tokens); } $objectToken = array_pop($tokens); if (!is_array($objectToken)) { return array(); } $objectName = str_replace('$', '', $objectToken[1]); try { $object = $this->getVariable($objectName); } catch (InvalidArgumentException $e) { return array(); } return array_filter( array_keys(get_class_vars(get_class($object))), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); $prevToken = array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } */ class ObjectMethodsMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $input = $this->getInput($tokens); $firstToken = array_pop($tokens); if (self::tokenIs($firstToken, self::T_STRING)) { // second token is the object operator array_pop($tokens); } $objectToken = array_pop($tokens); if (!is_array($objectToken)) { return array(); } $objectName = str_replace('$', '', $objectToken[1]); try { $object = $this->getVariable($objectName); } catch (InvalidArgumentException $e) { return array(); } return array_filter( get_class_methods($object), function ($var) use ($input) { return AbstractMatcher::startsWith($input, $var); } ); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); $prevToken = array_pop($tokens); switch (true) { case self::tokenIs($token, self::T_OBJECT_OPERATOR): case self::tokenIs($prevToken, self::T_OBJECT_OPERATOR): return true; } return false; } } */ class VariablesMatcher extends AbstractContextAwareMatcher { /** * {@inheritdoc} */ public function getMatches(array $tokens, array $info = array()) { $var = str_replace('$', '', $this->getInput($tokens)); return array_filter(array_keys($this->getVariables()), function ($variable) use ($var) { return AbstractMatcher::startsWith($var, $variable); }); } /** * {@inheritdoc} */ public function hasMatched(array $tokens) { $token = array_pop($tokens); switch (true) { case self::hasToken(array(self::T_OPEN_TAG, self::T_VARIABLE), $token): case is_string($token) && $token === '$': case self::isOperator($token): return true; } return false; } } * @author Justin Hileman */ class Docblock { /** * Tags in the docblock that have a whitespace-delimited number of parameters * (such as `@param type var desc` and `@return type desc`) and the names of * those parameters. * * @var array */ public static $vectors = array( 'throws' => array('type', 'desc'), 'param' => array('type', 'var', 'desc'), 'return' => array('type', 'desc'), ); protected $reflector; /** * The description of the symbol. * * @var string */ public $desc; /** * The tags defined in the docblock. * * The array has keys which are the tag names (excluding the @) and values * that are arrays, each of which is an entry for the tag. * * In the case where the tag name is defined in {@see DocBlock::$vectors} the * value within the tag-value array is an array in itself with keys as * described by {@see DocBlock::$vectors}. * * @var array */ public $tags; /** * The entire DocBlock comment that was parsed. * * @var string */ public $comment; /** * Docblock constructor. * * @param \Reflector $reflector */ public function __construct(\Reflector $reflector) { $this->reflector = $reflector; $this->setComment($reflector->getDocComment()); } /** * Set and parse the docblock comment. * * @param string $comment The docblock */ protected function setComment($comment) { $this->desc = ''; $this->tags = array(); $this->comment = $comment; $this->parseComment($comment); } /** * Find the length of the docblock prefix. * * @param array $lines * * @return int Prefix length */ protected static function prefixLength(array $lines) { // find only lines with interesting things $lines = array_filter($lines, function ($line) { return substr($line, strspn($line, "* \t\n\r\0\x0B")); }); // if we sort the lines, we only have to compare two items sort($lines); $first = reset($lines); $last = end($lines); // find the longest common substring $count = min(strlen($first), strlen($last)); for ($i = 0; $i < $count; $i++) { if ($first[$i] !== $last[$i]) { return $i; } } return $count; } /** * Parse the comment into the component parts and set the state of the object. * * @param string $comment The docblock */ protected function parseComment($comment) { // Strip the opening and closing tags of the docblock $comment = substr($comment, 3, -2); // Split into arrays of lines $comment = array_filter(preg_split('/\r?\n\r?/', $comment)); // Trim asterisks and whitespace from the beginning and whitespace from the end of lines $prefixLength = self::prefixLength($comment); $comment = array_map(function ($line) use ($prefixLength) { return rtrim(substr($line, $prefixLength)); }, $comment); // Group the lines together by @tags $blocks = array(); $b = -1; foreach ($comment as $line) { if (self::isTagged($line)) { $b++; $blocks[] = array(); } elseif ($b === -1) { $b = 0; $blocks[] = array(); } $blocks[$b][] = $line; } // Parse the blocks foreach ($blocks as $block => $body) { $body = trim(implode("\n", $body)); if ($block === 0 && !self::isTagged($body)) { // This is the description block $this->desc = $body; } else { // This block is tagged $tag = substr(self::strTag($body), 1); $body = ltrim(substr($body, strlen($tag) + 2)); if (isset(self::$vectors[$tag])) { // The tagged block is a vector $count = count(self::$vectors[$tag]); if ($body) { $parts = preg_split('/\s+/', $body, $count); } else { $parts = array(); } // Default the trailing values $parts = array_pad($parts, $count, null); // Store as a mapped array $this->tags[$tag][] = array_combine(self::$vectors[$tag], $parts); } else { // The tagged block is only text $this->tags[$tag][] = $body; } } } } /** * Whether or not a docblock contains a given @tag. * * @param string $tag The name of the @tag to check for * * @return bool */ public function hasTag($tag) { return is_array($this->tags) && array_key_exists($tag, $this->tags); } /** * The value of a tag. * * @param string $tag * * @return array */ public function tag($tag) { return $this->hasTag($tag) ? $this->tags[$tag] : null; } /** * Whether or not a string begins with a @tag. * * @param string $str * * @return bool */ public static function isTagged($str) { return isset($str[1]) && $str[0] === '@' && ctype_alpha($str[1]); } /** * The tag at the beginning of a string. * * @param string $str * * @return string|null */ public static function strTag($str) { if (preg_match('/^@[a-z0-9_]+/', $str, $matches)) { return $matches[0]; } } } =')) { $opt |= JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; } return json_encode($val, $opt); } } hasConstant($member)) { return new ReflectionConstant($class, $member); } elseif ($filter & self::METHOD && $class->hasMethod($member)) { return $class->getMethod($member); } elseif ($filter & self::PROPERTY && $class->hasProperty($member)) { return $class->getProperty($member); } elseif ($filter & self::STATIC_PROPERTY && $class->hasProperty($member) && $class->getProperty($member)->isStatic()) { return $class->getProperty($member); } else { throw new RuntimeException(sprintf( 'Unknown member %s on class %s', $member, is_object($value) ? get_class($value) : $value )); } } /** * Get a ReflectionClass (or ReflectionObject) if possible. * * @throws \InvalidArgumentException if $value is not a class name or instance * * @param mixed $value * * @return \ReflectionClass */ private static function getClass($value) { if (is_object($value)) { return new \ReflectionObject($value); } if (!is_string($value)) { throw new \InvalidArgumentException('Mirror expects an object or class'); } elseif (!class_exists($value) && !interface_exists($value) && !(function_exists('trait_exists') && trait_exists($value))) { throw new \InvalidArgumentException('Unknown class or function: ' . $value); } return new \ReflectionClass($value); } } filter = $filter; return parent::cloneVar($var, $filter); } /** * {@inheritdoc} */ protected function castResource(Stub $stub, $isNested) { return Caster::EXCLUDE_VERBOSE & $this->filter ? array() : parent::castResource($stub, $isNested); } } '\0', "\t" => '\t', "\n" => '\n', "\v" => '\v', "\f" => '\f', "\r" => '\r', "\033" => '\e', ); public function __construct(OutputFormatter $formatter) { $this->formatter = $formatter; parent::__construct(); $this->setColors(false); } /** * {@inheritdoc} */ public function enterHash(Cursor $cursor, $type, $class, $hasChild) { if (Cursor::HASH_INDEXED === $type || Cursor::HASH_ASSOC === $type) { $class = 0; } parent::enterHash($cursor, $type, $class, $hasChild); } /** * {@inheritdoc} */ protected function dumpKey(Cursor $cursor) { if (Cursor::HASH_INDEXED !== $cursor->hashType) { parent::dumpKey($cursor); } } protected function style($style, $value, $attr = array()) { if ('ref' === $style) { $value = strtr($value, '@', '#'); } $map = self::$controlCharsMap; $cchr = $this->styles['cchr']; $value = preg_replace_callback(self::$controlCharsRx, function ($c) use ($map, $cchr) { $s = ''; $i = 0; $c = $c[0]; do { $s .= isset($map[$c[$i]]) ? $map[$c[$i]] : sprintf('\x%02X', ord($c[$i])); } while (isset($c[++$i])); return "<{$cchr}>{$s}"; }, $this->formatter->escape($value)); $style = $this->styles[$style]; return "<{$style}>{$value}"; } /** * {@inheritdoc} */ protected function dumpLine($depth, $endOfValue = false) { if ($endOfValue && 0 < $depth) { $this->line .= ','; } $this->line = $this->formatter->format($this->line); parent::dumpLine($depth, $endOfValue); } } 'number', 'const' => 'const', 'str' => 'string', 'cchr' => 'default', 'note' => 'class', 'ref' => 'default', 'public' => 'public', 'protected' => 'protected', 'private' => 'private', 'meta' => 'comment', 'key' => 'comment', 'index' => 'number', ); public function __construct(OutputFormatter $formatter) { $this->dumper = new Dumper($formatter); $this->dumper->setStyles($this->styles); $this->cloner = new Cloner(); $this->cloner->addCasters(array('*' => function ($obj, array $a, Stub $stub, $isNested, $filter = 0) { if ($filter || $isNested) { if ($obj instanceof \Exception) { $a = Caster::filter($a, Caster::EXCLUDE_NOT_IMPORTANT | Caster::EXCLUDE_EMPTY, $this->exceptionsImportants); } else { $a = Caster::filter($a, Caster::EXCLUDE_PROTECTED | Caster::EXCLUDE_PRIVATE); } } return $a; })); } /** * Register casters. * * @see http://symfony.com/doc/current/components/var_dumper/advanced.html#casters * * @param callable[] $casters A map of casters */ public function addCasters(array $casters) { $this->cloner->addCasters($casters); } /** * Present a reference to the value. * * @param mixed $value * * @return string */ public function presentRef($value) { return $this->present($value, 0); } /** * Present a full representation of the value. * * If $depth is 0, the value will be presented as a ref instead. * * @param mixed $value * @param int $depth (default: null) * @param int $options One of Presenter constants * * @return string */ public function present($value, $depth = null, $options = 0) { $data = $this->cloner->cloneVar($value, !($options & self::VERBOSE) ? Caster::EXCLUDE_VERBOSE : 0); if (null !== $depth) { $data = $data->withMaxDepth($depth); } $output = ''; $this->dumper->dump($data, function ($line, $depth) use (&$output) { if ($depth >= 0) { if ('' !== $output) { $output .= PHP_EOL; } $output .= str_repeat(' ', $depth) . $line; } }); return OutputFormatter::escape($output); } } getLatest(), '>='); } /** * @return string */ public function getLatest() { if (!isset($this->latest)) { $this->setLatest($this->getVersionFromTag()); } return $this->latest; } /** * @param string $version */ public function setLatest($version) { $this->latest = $version; } /** * @return string|null */ private function getVersionFromTag() { $contents = $this->fetchLatestRelease(); if (!$contents || !isset($contents->tag_name)) { throw new \InvalidArgumentException('Unable to check for updates'); } $this->setLatest($contents->tag_name); return $this->getLatest(); } /** * Set to public to make testing easier. * * @return mixed */ public function fetchLatestRelease() { $context = stream_context_create(array('http' => array('user_agent' => 'PsySH/' . Shell::VERSION))); return json_decode(@file_get_contents(self::URL, false, $context)); } } cacheFile = $cacheFile; $this->interval = $interval; } public function fetchLatestRelease() { // Read the cached file $cached = json_decode(@file_get_contents($this->cacheFile, false)); if ($cached && isset($cached->last_check) && isset($cached->release)) { $now = new \DateTime(); $lastCheck = new \DateTime($cached->last_check); if ($lastCheck >= $now->sub($this->getDateInterval())) { return $cached->release; } } // Fall back to fetching from GitHub $release = parent::fetchLatestRelease(); if ($release && isset($release->tag_name)) { $this->updateCache($release); } return $release; } private function getDateInterval() { switch ($this->interval) { case Checker::DAILY: return new \DateInterval('P1D'); case Checker::WEEKLY: return new \DateInterval('P1W'); case Checker::MONTHLY: return new \DateInterval('P1M'); } } private function updateCache($release) { $data = array( 'last_check' => date(DATE_ATOM), 'release' => $release, ); file_put_contents($this->cacheFile, json_encode($data)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares arrays for equality. */ class ArrayComparator extends Comparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { return is_array($expected) && is_array($actual); } /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @param array $processed * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false, array &$processed = array()) { if ($canonicalize) { sort($expected); sort($actual); } $remaining = $actual; $expString = $actString = "Array (\n"; $equal = true; foreach ($expected as $key => $value) { unset($remaining[$key]); if (!array_key_exists($key, $actual)) { $expString .= sprintf( " %s => %s\n", $this->exporter->export($key), $this->exporter->shortenedExport($value) ); $equal = false; continue; } try { $comparator = $this->factory->getComparatorFor($value, $actual[$key]); $comparator->assertEquals($value, $actual[$key], $delta, $canonicalize, $ignoreCase, $processed); $expString .= sprintf( " %s => %s\n", $this->exporter->export($key), $this->exporter->shortenedExport($value) ); $actString .= sprintf( " %s => %s\n", $this->exporter->export($key), $this->exporter->shortenedExport($actual[$key]) ); } catch (ComparisonFailure $e) { $expString .= sprintf( " %s => %s\n", $this->exporter->export($key), $e->getExpectedAsString() ? $this->indent($e->getExpectedAsString()) : $this->exporter->shortenedExport($e->getExpected()) ); $actString .= sprintf( " %s => %s\n", $this->exporter->export($key), $e->getActualAsString() ? $this->indent($e->getActualAsString()) : $this->exporter->shortenedExport($e->getActual()) ); $equal = false; } } foreach ($remaining as $key => $value) { $actString .= sprintf( " %s => %s\n", $this->exporter->export($key), $this->exporter->shortenedExport($value) ); $equal = false; } $expString .= ')'; $actString .= ')'; if (!$equal) { throw new ComparisonFailure( $expected, $actual, $expString, $actString, false, 'Failed asserting that two arrays are equal.' ); } } protected function indent($lines) { return trim(str_replace("\n", "\n ", $lines)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use SebastianBergmann\Exporter\Exporter; /** * Abstract base class for comparators which compare values for equality. */ abstract class Comparator { /** * @var Factory */ protected $factory; /** * @var Exporter */ protected $exporter; public function __construct() { $this->exporter = new Exporter; } /** * @param Factory $factory */ public function setFactory(Factory $factory) { $this->factory = $factory; } /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ abstract public function accepts($expected, $actual); /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ abstract public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use SebastianBergmann\Diff\Differ; /** * Thrown when an assertion for string equality failed. */ class ComparisonFailure extends \RuntimeException { /** * Expected value of the retrieval which does not match $actual. * @var mixed */ protected $expected; /** * Actually retrieved value which does not match $expected. * @var mixed */ protected $actual; /** * The string representation of the expected value * @var string */ protected $expectedAsString; /** * The string representation of the actual value * @var string */ protected $actualAsString; /** * @var bool */ protected $identical; /** * Optional message which is placed in front of the first line * returned by toString(). * @var string */ protected $message; /** * Initialises with the expected value and the actual value. * * @param mixed $expected Expected value retrieved. * @param mixed $actual Actual value retrieved. * @param string $expectedAsString * @param string $actualAsString * @param bool $identical * @param string $message A string which is prefixed on all returned lines * in the difference output. */ public function __construct($expected, $actual, $expectedAsString, $actualAsString, $identical = false, $message = '') { $this->expected = $expected; $this->actual = $actual; $this->expectedAsString = $expectedAsString; $this->actualAsString = $actualAsString; $this->message = $message; } /** * @return mixed */ public function getActual() { return $this->actual; } /** * @return mixed */ public function getExpected() { return $this->expected; } /** * @return string */ public function getActualAsString() { return $this->actualAsString; } /** * @return string */ public function getExpectedAsString() { return $this->expectedAsString; } /** * @return string */ public function getDiff() { if (!$this->actualAsString && !$this->expectedAsString) { return ''; } $differ = new Differ("\n--- Expected\n+++ Actual\n"); return $differ->diff($this->expectedAsString, $this->actualAsString); } /** * @return string */ public function toString() { return $this->message . $this->getDiff(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares DateTimeInterface instances for equality. */ class DateTimeComparator extends ObjectComparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { return ($expected instanceof \DateTime || $expected instanceof \DateTimeInterface) && ($actual instanceof \DateTime || $actual instanceof \DateTimeInterface); } /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) { $delta = new \DateInterval(sprintf('PT%sS', abs($delta))); $expectedLower = clone $expected; $expectedUpper = clone $expected; if ($actual < $expectedLower->sub($delta) || $actual > $expectedUpper->add($delta)) { throw new ComparisonFailure( $expected, $actual, $this->dateTimeToString($expected), $this->dateTimeToString($actual), false, 'Failed asserting that two DateTime objects are equal.' ); } } /** * Returns an ISO 8601 formatted string representation of a datetime or * 'Invalid DateTimeInterface object' if the provided DateTimeInterface was not properly * initialized. * * @param \DateTimeInterface $datetime * @return string */ private function dateTimeToString($datetime) { $string = $datetime->format('Y-m-d\TH:i:s.uO'); return $string ? $string : 'Invalid DateTimeInterface object'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; use DOMDocument; use DOMNode; /** * Compares DOMNode instances for equality. */ class DOMNodeComparator extends ObjectComparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { return $expected instanceof DOMNode && $actual instanceof DOMNode; } /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) { $expectedAsString = $this->nodeToText($expected, true, $ignoreCase); $actualAsString = $this->nodeToText($actual, true, $ignoreCase); if ($expectedAsString !== $actualAsString) { if ($expected instanceof DOMDocument) { $type = 'documents'; } else { $type = 'nodes'; } throw new ComparisonFailure( $expected, $actual, $expectedAsString, $actualAsString, false, sprintf("Failed asserting that two DOM %s are equal.\n", $type) ); } } /** * Returns the normalized, whitespace-cleaned, and indented textual * representation of a DOMNode. * * @param DOMNode $node * @param bool $canonicalize * @param bool $ignoreCase * @return string */ private function nodeToText(DOMNode $node, $canonicalize, $ignoreCase) { if ($canonicalize) { $document = new DOMDocument; $document->loadXML($node->C14N()); $node = $document; } if ($node instanceof DOMDocument) { $document = $node; } else { $document = $node->ownerDocument; } $document->formatOutput = true; $document->normalizeDocument(); if ($node instanceof DOMDocument) { $text = $node->saveXML(); } else { $text = $document->saveXML($node); } if ($ignoreCase) { $text = strtolower($text); } return $text; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares doubles for equality. */ class DoubleComparator extends NumericComparator { /** * Smallest value available in PHP. * * @var float */ const EPSILON = 0.0000000001; /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { return (is_double($expected) || is_double($actual)) && is_numeric($expected) && is_numeric($actual); } /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) { if ($delta == 0) { $delta = self::EPSILON; } parent::assertEquals($expected, $actual, $delta, $canonicalize, $ignoreCase); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares Exception instances for equality. */ class ExceptionComparator extends ObjectComparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { return $expected instanceof \Exception && $actual instanceof \Exception; } /** * Converts an object to an array containing all of its private, protected * and public properties. * * @param object $object * @return array */ protected function toArray($object) { $array = parent::toArray($object); unset( $array['file'], $array['line'], $array['trace'], $array['string'], $array['xdebug_message'] ); return $array; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Factory for comparators which compare values for equality. */ class Factory { /** * @var Comparator[] */ private $comparators = array(); /** * @var Factory */ private static $instance; /** * Constructs a new factory. */ public function __construct() { $this->register(new TypeComparator); $this->register(new ScalarComparator); $this->register(new NumericComparator); $this->register(new DoubleComparator); $this->register(new ArrayComparator); $this->register(new ResourceComparator); $this->register(new ObjectComparator); $this->register(new ExceptionComparator); $this->register(new SplObjectStorageComparator); $this->register(new DOMNodeComparator); $this->register(new MockObjectComparator); $this->register(new DateTimeComparator); } /** * @return Factory */ public static function getInstance() { if (self::$instance === null) { self::$instance = new self; } return self::$instance; } /** * Returns the correct comparator for comparing two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return Comparator */ public function getComparatorFor($expected, $actual) { foreach ($this->comparators as $comparator) { if ($comparator->accepts($expected, $actual)) { return $comparator; } } } /** * Registers a new comparator. * * This comparator will be returned by getInstance() if its accept() method * returns TRUE for the compared values. It has higher priority than the * existing comparators, meaning that its accept() method will be tested * before those of the other comparators. * * @param Comparator $comparator The registered comparator */ public function register(Comparator $comparator) { array_unshift($this->comparators, $comparator); $comparator->setFactory($this); } /** * Unregisters a comparator. * * This comparator will no longer be returned by getInstance(). * * @param Comparator $comparator The unregistered comparator */ public function unregister(Comparator $comparator) { foreach ($this->comparators as $key => $_comparator) { if ($comparator === $_comparator) { unset($this->comparators[$key]); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares PHPUnit_Framework_MockObject_MockObject instances for equality. */ class MockObjectComparator extends ObjectComparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { return $expected instanceof \PHPUnit_Framework_MockObject_MockObject && $actual instanceof \PHPUnit_Framework_MockObject_MockObject; } /** * Converts an object to an array containing all of its private, protected * and public properties. * * @param object $object * @return array */ protected function toArray($object) { $array = parent::toArray($object); unset($array['__phpunit_invocationMocker']); return $array; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares numerical values for equality. */ class NumericComparator extends ScalarComparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { // all numerical values, but not if one of them is a double // or both of them are strings return is_numeric($expected) && is_numeric($actual) && !(is_double($expected) || is_double($actual)) && !(is_string($expected) && is_string($actual)); } /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) { if (is_infinite($actual) && is_infinite($expected)) { return; } if ((is_infinite($actual) xor is_infinite($expected)) || (is_nan($actual) or is_nan($expected)) || abs($actual - $expected) > $delta) { throw new ComparisonFailure( $expected, $actual, '', '', false, sprintf( 'Failed asserting that %s matches expected %s.', $this->exporter->export($actual), $this->exporter->export($expected) ) ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares objects for equality. */ class ObjectComparator extends ArrayComparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { return is_object($expected) && is_object($actual); } /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @param array $processed * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false, array &$processed = array()) { if (get_class($actual) !== get_class($expected)) { throw new ComparisonFailure( $expected, $actual, $this->exporter->export($expected), $this->exporter->export($actual), false, sprintf( '%s is not instance of expected class "%s".', $this->exporter->export($actual), get_class($expected) ) ); } // don't compare twice to allow for cyclic dependencies if (in_array(array($actual, $expected), $processed, true) || in_array(array($expected, $actual), $processed, true)) { return; } $processed[] = array($actual, $expected); // don't compare objects if they are identical // this helps to avoid the error "maximum function nesting level reached" // CAUTION: this conditional clause is not tested if ($actual !== $expected) { try { parent::assertEquals( $this->toArray($expected), $this->toArray($actual), $delta, $canonicalize, $ignoreCase, $processed ); } catch (ComparisonFailure $e) { throw new ComparisonFailure( $expected, $actual, // replace "Array" with "MyClass object" substr_replace($e->getExpectedAsString(), get_class($expected) . ' Object', 0, 5), substr_replace($e->getActualAsString(), get_class($actual) . ' Object', 0, 5), false, 'Failed asserting that two objects are equal.' ); } } } /** * Converts an object to an array containing all of its private, protected * and public properties. * * @param object $object * @return array */ protected function toArray($object) { return $this->exporter->toArray($object); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares resources for equality. */ class ResourceComparator extends Comparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { return is_resource($expected) && is_resource($actual); } /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) { if ($actual != $expected) { throw new ComparisonFailure( $expected, $actual, $this->exporter->export($expected), $this->exporter->export($actual) ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares scalar or NULL values for equality. */ class ScalarComparator extends Comparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool * @since Method available since Release 3.6.0 */ public function accepts($expected, $actual) { return ((is_scalar($expected) xor null === $expected) && (is_scalar($actual) xor null === $actual)) // allow comparison between strings and objects featuring __toString() || (is_string($expected) && is_object($actual) && method_exists($actual, '__toString')) || (is_object($expected) && method_exists($expected, '__toString') && is_string($actual)); } /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) { $expectedToCompare = $expected; $actualToCompare = $actual; // always compare as strings to avoid strange behaviour // otherwise 0 == 'Foobar' if (is_string($expected) || is_string($actual)) { $expectedToCompare = (string) $expectedToCompare; $actualToCompare = (string) $actualToCompare; if ($ignoreCase) { $expectedToCompare = strtolower($expectedToCompare); $actualToCompare = strtolower($actualToCompare); } } if ($expectedToCompare != $actualToCompare) { if (is_string($expected) && is_string($actual)) { throw new ComparisonFailure( $expected, $actual, $this->exporter->export($expected), $this->exporter->export($actual), false, 'Failed asserting that two strings are equal.' ); } throw new ComparisonFailure( $expected, $actual, // no diff is required '', '', false, sprintf( 'Failed asserting that %s matches expected %s.', $this->exporter->export($actual), $this->exporter->export($expected) ) ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares \SplObjectStorage instances for equality. */ class SplObjectStorageComparator extends Comparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { return $expected instanceof \SplObjectStorage && $actual instanceof \SplObjectStorage; } /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) { foreach ($actual as $object) { if (!$expected->contains($object)) { throw new ComparisonFailure( $expected, $actual, $this->exporter->export($expected), $this->exporter->export($actual), false, 'Failed asserting that two objects are equal.' ); } } foreach ($expected as $object) { if (!$actual->contains($object)) { throw new ComparisonFailure( $expected, $actual, $this->exporter->export($expected), $this->exporter->export($actual), false, 'Failed asserting that two objects are equal.' ); } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Comparator; /** * Compares values for type equality. */ class TypeComparator extends Comparator { /** * Returns whether the comparator can compare two values. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @return bool */ public function accepts($expected, $actual) { return true; } /** * Asserts that two values are equal. * * @param mixed $expected The first value to compare * @param mixed $actual The second value to compare * @param float $delta The allowed numerical distance between two values to * consider them equal * @param bool $canonicalize If set to TRUE, arrays are sorted before * comparison * @param bool $ignoreCase If set to TRUE, upper- and lowercasing is * ignored when comparing string values * @throws ComparisonFailure Thrown when the comparison * fails. Contains information about the * specific errors that lead to the failure. */ public function assertEquals($expected, $actual, $delta = 0.0, $canonicalize = false, $ignoreCase = false) { if (gettype($expected) != gettype($actual)) { throw new ComparisonFailure( $expected, $actual, // we don't need a diff '', '', false, sprintf( '%s does not match expected type "%s".', $this->exporter->shortenedExport($actual), gettype($expected) ) ); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; /** */ class Chunk { /** * @var int */ private $start; /** * @var int */ private $startRange; /** * @var int */ private $end; /** * @var int */ private $endRange; /** * @var array */ private $lines; /** * @param int $start * @param int $startRange * @param int $end * @param int $endRange * @param array $lines */ public function __construct($start = 0, $startRange = 1, $end = 0, $endRange = 1, array $lines = array()) { $this->start = (int) $start; $this->startRange = (int) $startRange; $this->end = (int) $end; $this->endRange = (int) $endRange; $this->lines = $lines; } /** * @return int */ public function getStart() { return $this->start; } /** * @return int */ public function getStartRange() { return $this->startRange; } /** * @return int */ public function getEnd() { return $this->end; } /** * @return int */ public function getEndRange() { return $this->endRange; } /** * @return array */ public function getLines() { return $this->lines; } /** * @param array $lines */ public function setLines(array $lines) { $this->lines = $lines; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; /** */ class Diff { /** * @var string */ private $from; /** * @var string */ private $to; /** * @var Chunk[] */ private $chunks; /** * @param string $from * @param string $to * @param Chunk[] $chunks */ public function __construct($from, $to, array $chunks = array()) { $this->from = $from; $this->to = $to; $this->chunks = $chunks; } /** * @return string */ public function getFrom() { return $this->from; } /** * @return string */ public function getTo() { return $this->to; } /** * @return Chunk[] */ public function getChunks() { return $this->chunks; } /** * @param Chunk[] $chunks */ public function setChunks(array $chunks) { $this->chunks = $chunks; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; use SebastianBergmann\Diff\LCS\LongestCommonSubsequence; use SebastianBergmann\Diff\LCS\TimeEfficientImplementation; use SebastianBergmann\Diff\LCS\MemoryEfficientImplementation; /** * Diff implementation. */ class Differ { /** * @var string */ private $header; /** * @var bool */ private $showNonDiffLines; /** * @param string $header */ public function __construct($header = "--- Original\n+++ New\n", $showNonDiffLines = true) { $this->header = $header; $this->showNonDiffLines = $showNonDiffLines; } /** * Returns the diff between two arrays or strings as string. * * @param array|string $from * @param array|string $to * @param LongestCommonSubsequence $lcs * * @return string */ public function diff($from, $to, LongestCommonSubsequence $lcs = null) { if (!is_array($from) && !is_string($from)) { $from = (string) $from; } if (!is_array($to) && !is_string($to)) { $to = (string) $to; } $buffer = $this->header; $diff = $this->diffToArray($from, $to, $lcs); $inOld = false; $i = 0; $old = array(); foreach ($diff as $line) { if ($line[1] === 0 /* OLD */) { if ($inOld === false) { $inOld = $i; } } elseif ($inOld !== false) { if (($i - $inOld) > 5) { $old[$inOld] = $i - 1; } $inOld = false; } ++$i; } $start = isset($old[0]) ? $old[0] : 0; $end = count($diff); if ($tmp = array_search($end, $old)) { $end = $tmp; } $newChunk = true; for ($i = $start; $i < $end; $i++) { if (isset($old[$i])) { $buffer .= "\n"; $newChunk = true; $i = $old[$i]; } if ($newChunk) { if ($this->showNonDiffLines === true) { $buffer .= "@@ @@\n"; } $newChunk = false; } if ($diff[$i][1] === 1 /* ADDED */) { $buffer .= '+' . $diff[$i][0] . "\n"; } elseif ($diff[$i][1] === 2 /* REMOVED */) { $buffer .= '-' . $diff[$i][0] . "\n"; } elseif ($this->showNonDiffLines === true) { $buffer .= ' ' . $diff[$i][0] . "\n"; } } return $buffer; } /** * Returns the diff between two arrays or strings as array. * * Each array element contains two elements: * - [0] => string $token * - [1] => 2|1|0 * * - 2: REMOVED: $token was removed from $from * - 1: ADDED: $token was added to $from * - 0: OLD: $token is not changed in $to * * @param array|string $from * @param array|string $to * @param LongestCommonSubsequence $lcs * * @return array */ public function diffToArray($from, $to, LongestCommonSubsequence $lcs = null) { preg_match_all('(\r\n|\r|\n)', $from, $fromMatches); preg_match_all('(\r\n|\r|\n)', $to, $toMatches); if (is_string($from)) { $from = preg_split('(\r\n|\r|\n)', $from); } if (is_string($to)) { $to = preg_split('(\r\n|\r|\n)', $to); } $start = array(); $end = array(); $fromLength = count($from); $toLength = count($to); $length = min($fromLength, $toLength); for ($i = 0; $i < $length; ++$i) { if ($from[$i] === $to[$i]) { $start[] = $from[$i]; unset($from[$i], $to[$i]); } else { break; } } $length -= $i; for ($i = 1; $i < $length; ++$i) { if ($from[$fromLength - $i] === $to[$toLength - $i]) { array_unshift($end, $from[$fromLength - $i]); unset($from[$fromLength - $i], $to[$toLength - $i]); } else { break; } } if ($lcs === null) { $lcs = $this->selectLcsImplementation($from, $to); } $common = $lcs->calculate(array_values($from), array_values($to)); $diff = array(); if (isset($fromMatches[0]) && $toMatches[0] && count($fromMatches[0]) === count($toMatches[0]) && $fromMatches[0] !== $toMatches[0]) { $diff[] = array( '#Warning: Strings contain different line endings!', 0 ); } foreach ($start as $token) { $diff[] = array($token, 0 /* OLD */); } reset($from); reset($to); foreach ($common as $token) { while ((($fromToken = reset($from)) !== $token)) { $diff[] = array(array_shift($from), 2 /* REMOVED */); } while ((($toToken = reset($to)) !== $token)) { $diff[] = array(array_shift($to), 1 /* ADDED */); } $diff[] = array($token, 0 /* OLD */); array_shift($from); array_shift($to); } while (($token = array_shift($from)) !== null) { $diff[] = array($token, 2 /* REMOVED */); } while (($token = array_shift($to)) !== null) { $diff[] = array($token, 1 /* ADDED */); } foreach ($end as $token) { $diff[] = array($token, 0 /* OLD */); } return $diff; } /** * @param array $from * @param array $to * * @return LongestCommonSubsequence */ private function selectLcsImplementation(array $from, array $to) { // We do not want to use the time-efficient implementation if its memory // footprint will probably exceed this value. Note that the footprint // calculation is only an estimation for the matrix and the LCS method // will typically allocate a bit more memory than this. $memoryLimit = 100 * 1024 * 1024; if ($this->calculateEstimatedFootprint($from, $to) > $memoryLimit) { return new MemoryEfficientImplementation; } return new TimeEfficientImplementation; } /** * Calculates the estimated memory footprint for the DP-based method. * * @param array $from * @param array $to * * @return int */ private function calculateEstimatedFootprint(array $from, array $to) { $itemSize = PHP_INT_SIZE == 4 ? 76 : 144; return $itemSize * pow(min(count($from), count($to)), 2); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\LCS; /** * Interface for implementations of longest common subsequence calculation. */ interface LongestCommonSubsequence { /** * Calculates the longest common subsequence of two arrays. * * @param array $from * @param array $to * * @return array */ public function calculate(array $from, array $to); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\LCS; /** * Memory-efficient implementation of longest common subsequence calculation. */ class MemoryEfficientImplementation implements LongestCommonSubsequence { /** * Calculates the longest common subsequence of two arrays. * * @param array $from * @param array $to * * @return array */ public function calculate(array $from, array $to) { $cFrom = count($from); $cTo = count($to); if ($cFrom == 0) { return array(); } elseif ($cFrom == 1) { if (in_array($from[0], $to)) { return array($from[0]); } else { return array(); } } else { $i = intval($cFrom / 2); $fromStart = array_slice($from, 0, $i); $fromEnd = array_slice($from, $i); $llB = $this->length($fromStart, $to); $llE = $this->length(array_reverse($fromEnd), array_reverse($to)); $jMax = 0; $max = 0; for ($j = 0; $j <= $cTo; $j++) { $m = $llB[$j] + $llE[$cTo - $j]; if ($m >= $max) { $max = $m; $jMax = $j; } } $toStart = array_slice($to, 0, $jMax); $toEnd = array_slice($to, $jMax); return array_merge( $this->calculate($fromStart, $toStart), $this->calculate($fromEnd, $toEnd) ); } } /** * @param array $from * @param array $to * * @return array */ private function length(array $from, array $to) { $current = array_fill(0, count($to) + 1, 0); $cFrom = count($from); $cTo = count($to); for ($i = 0; $i < $cFrom; $i++) { $prev = $current; for ($j = 0; $j < $cTo; $j++) { if ($from[$i] == $to[$j]) { $current[$j + 1] = $prev[$j] + 1; } else { $current[$j + 1] = max($current[$j], $prev[$j + 1]); } } } return $current; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff\LCS; /** * Time-efficient implementation of longest common subsequence calculation. */ class TimeEfficientImplementation implements LongestCommonSubsequence { /** * Calculates the longest common subsequence of two arrays. * * @param array $from * @param array $to * * @return array */ public function calculate(array $from, array $to) { $common = array(); $fromLength = count($from); $toLength = count($to); $width = $fromLength + 1; $matrix = new \SplFixedArray($width * ($toLength + 1)); for ($i = 0; $i <= $fromLength; ++$i) { $matrix[$i] = 0; } for ($j = 0; $j <= $toLength; ++$j) { $matrix[$j * $width] = 0; } for ($i = 1; $i <= $fromLength; ++$i) { for ($j = 1; $j <= $toLength; ++$j) { $o = ($j * $width) + $i; $matrix[$o] = max( $matrix[$o - 1], $matrix[$o - $width], $from[$i - 1] === $to[$j - 1] ? $matrix[$o - $width - 1] + 1 : 0 ); } } $i = $fromLength; $j = $toLength; while ($i > 0 && $j > 0) { if ($from[$i-1] === $to[$j-1]) { $common[] = $from[$i-1]; --$i; --$j; } else { $o = ($j * $width) + $i; if ($matrix[$o - $width] > $matrix[$o - 1]) { --$j; } else { --$i; } } } return array_reverse($common); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; /** */ class Line { const ADDED = 1; const REMOVED = 2; const UNCHANGED = 3; /** * @var int */ private $type; /** * @var string */ private $content; /** * @param int $type * @param string $content */ public function __construct($type = self::UNCHANGED, $content = '') { $this->type = $type; $this->content = $content; } /** * @return string */ public function getContent() { return $this->content; } /** * @return int */ public function getType() { return $this->type; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Diff; /** * Unified diff parser. */ class Parser { /** * @param string $string * * @return Diff[] */ public function parse($string) { $lines = preg_split('(\r\n|\r|\n)', $string); $lineCount = count($lines); $diffs = array(); $diff = null; $collected = array(); for ($i = 0; $i < $lineCount; ++$i) { if (preg_match('(^---\\s+(?P\\S+))', $lines[$i], $fromMatch) && preg_match('(^\\+\\+\\+\\s+(?P\\S+))', $lines[$i + 1], $toMatch)) { if ($diff !== null) { $this->parseFileDiff($diff, $collected); $diffs[] = $diff; $collected = array(); } $diff = new Diff($fromMatch['file'], $toMatch['file']); ++$i; } else { if (preg_match('/^(?:diff --git |index [\da-f\.]+|[+-]{3} [ab])/', $lines[$i])) { continue; } $collected[] = $lines[$i]; } } if (count($collected) && ($diff !== null)) { $this->parseFileDiff($diff, $collected); $diffs[] = $diff; } return $diffs; } /** * @param Diff $diff * @param array $lines */ private function parseFileDiff(Diff $diff, array $lines) { $chunks = array(); foreach ($lines as $line) { if (preg_match('/^@@\s+-(?P\d+)(?:,\s*(?P\d+))?\s+\+(?P\d+)(?:,\s*(?P\d+))?\s+@@/', $line, $match)) { $chunk = new Chunk( $match['start'], isset($match['startrange']) ? max(1, $match['startrange']) : 1, $match['end'], isset($match['endrange']) ? max(1, $match['endrange']) : 1 ); $chunks[] = $chunk; $diffLines = array(); continue; } if (preg_match('/^(?P[+ -])?(?P.*)/', $line, $match)) { $type = Line::UNCHANGED; if ($match['type'] == '+') { $type = Line::ADDED; } elseif ($match['type'] == '-') { $type = Line::REMOVED; } $diffLines[] = new Line($type, $match['line']); if (isset($chunk)) { $chunk->setLines($diffLines); } } } $diff->setChunks($chunks); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Environment; /** */ class Console { const STDIN = 0; const STDOUT = 1; const STDERR = 2; /** * Returns true if STDOUT supports colorization. * * This code has been copied and adapted from * Symfony\Component\Console\Output\OutputStream. * * @return bool */ public function hasColorSupport() { if (DIRECTORY_SEPARATOR == '\\') { return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); } if (!defined('STDOUT')) { return false; } return $this->isInteractive(STDOUT); } /** * Returns the number of columns of the terminal. * * @return int */ public function getNumberOfColumns() { if (DIRECTORY_SEPARATOR == '\\') { $columns = 80; if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { $columns = $matches[1]; } elseif (function_exists('proc_open')) { $process = proc_open( 'mode CON', array( 1 => array('pipe', 'w'), 2 => array('pipe', 'w') ), $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)) { $columns = $matches[2]; } } } return $columns - 1; } if (!$this->isInteractive(self::STDIN)) { return 80; } if (function_exists('shell_exec') && preg_match('#\d+ (\d+)#', shell_exec('stty size'), $match) === 1) { if ((int) $match[1] > 0) { return (int) $match[1]; } } if (function_exists('shell_exec') && preg_match('#columns = (\d+);#', shell_exec('stty'), $match) === 1) { if ((int) $match[1] > 0) { return (int) $match[1]; } } return 80; } /** * Returns if the file descriptor is an interactive terminal or not. * * @param int|resource $fileDescriptor * * @return bool */ public function isInteractive($fileDescriptor = self::STDOUT) { return function_exists('posix_isatty') && @posix_isatty($fileDescriptor); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Environment; /** * Utility class for HHVM/PHP environment handling. */ class Runtime { /** * @var string */ private static $binary; /** * Returns true when Xdebug is supported or * the runtime used is PHPDBG (PHP >= 7.0). * * @return bool */ public function canCollectCodeCoverage() { return $this->hasXdebug() || $this->hasPHPDBGCodeCoverage(); } /** * Returns the path to the binary of the current runtime. * Appends ' --php' to the path when the runtime is HHVM. * * @return string */ public function getBinary() { // HHVM if (self::$binary === null && $this->isHHVM()) { if ((self::$binary = getenv('PHP_BINARY')) === false) { self::$binary = PHP_BINARY; } self::$binary = escapeshellarg(self::$binary) . ' --php'; } // PHP >= 5.4.0 if (self::$binary === null && defined('PHP_BINARY')) { if (PHP_BINARY !== '') { self::$binary = escapeshellarg(PHP_BINARY); } } // PHP < 5.4.0 if (self::$binary === null) { if (PHP_SAPI == 'cli' && isset($_SERVER['_'])) { if (strpos($_SERVER['_'], 'phpunit') !== false) { $file = file($_SERVER['_']); if (strpos($file[0], ' ') !== false) { $tmp = explode(' ', $file[0]); self::$binary = escapeshellarg(trim($tmp[1])); } else { self::$binary = escapeshellarg(ltrim(trim($file[0]), '#!')); } } elseif (strpos(basename($_SERVER['_']), 'php') !== false) { self::$binary = escapeshellarg($_SERVER['_']); } } } if (self::$binary === null) { $possibleBinaryLocations = array( PHP_BINDIR . '/php', PHP_BINDIR . '/php-cli.exe', PHP_BINDIR . '/php.exe' ); foreach ($possibleBinaryLocations as $binary) { if (is_readable($binary)) { self::$binary = escapeshellarg($binary); break; } } } if (self::$binary === null) { self::$binary = 'php'; } return self::$binary; } /** * @return string */ public function getNameWithVersion() { return $this->getName() . ' ' . $this->getVersion(); } /** * @return string */ public function getName() { if ($this->isHHVM()) { return 'HHVM'; } elseif ($this->isPHPDBG()) { return 'PHPDBG'; } else { return 'PHP'; } } /** * @return string */ public function getVendorUrl() { if ($this->isHHVM()) { return 'http://hhvm.com/'; } else { return 'https://secure.php.net/'; } } /** * @return string */ public function getVersion() { if ($this->isHHVM()) { return HHVM_VERSION; } else { return PHP_VERSION; } } /** * Returns true when the runtime used is PHP and Xdebug is loaded. * * @return bool */ public function hasXdebug() { return ($this->isPHP() || $this->isHHVM()) && extension_loaded('xdebug'); } /** * Returns true when the runtime used is HHVM. * * @return bool */ public function isHHVM() { return defined('HHVM_VERSION'); } /** * Returns true when the runtime used is PHP without the PHPDBG SAPI. * * @return bool */ public function isPHP() { return !$this->isHHVM() && !$this->isPHPDBG(); } /** * Returns true when the runtime used is PHP with the PHPDBG SAPI. * * @return bool */ public function isPHPDBG() { return PHP_SAPI === 'phpdbg' && !$this->isHHVM(); } /** * Returns true when the runtime used is PHP with the PHPDBG SAPI * and the phpdbg_*_oplog() functions are available (PHP >= 7.0). * * @return bool */ public function hasPHPDBGCodeCoverage() { return $this->isPHPDBG() && function_exists('phpdbg_start_oplog'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\Exporter; use SebastianBergmann\RecursionContext\Context; /** * A nifty utility for visualizing PHP variables. * * * export(new Exception); * */ class Exporter { /** * Exports a value as a string * * The output of this method is similar to the output of print_r(), but * improved in various aspects: * * - NULL is rendered as "null" (instead of "") * - TRUE is rendered as "true" (instead of "1") * - FALSE is rendered as "false" (instead of "") * - Strings are always quoted with single quotes * - Carriage returns and newlines are normalized to \n * - Recursion and repeated rendering is treated properly * * @param mixed $value * @param int $indentation The indentation level of the 2nd+ line * @return string */ public function export($value, $indentation = 0) { return $this->recursiveExport($value, $indentation); } /** * @param mixed $data * @param Context $context * @return string */ public function shortenedRecursiveExport(&$data, Context $context = null) { $result = array(); $exporter = new self(); if (!$context) { $context = new Context; } $context->add($data); foreach ($data as $key => $value) { if (is_array($value)) { if ($context->contains($data[$key]) !== false) { $result[] = '*RECURSION*'; } else { $result[] = sprintf( 'array(%s)', $this->shortenedRecursiveExport($data[$key], $context) ); } } else { $result[] = $exporter->shortenedExport($value); } } return implode(', ', $result); } /** * Exports a value into a single-line string * * The output of this method is similar to the output of * SebastianBergmann\Exporter\Exporter::export(). * * Newlines are replaced by the visible string '\n'. * Contents of arrays and objects (if any) are replaced by '...'. * * @param mixed $value * @return string * @see SebastianBergmann\Exporter\Exporter::export */ public function shortenedExport($value) { if (is_string($value)) { $string = $this->export($value); if (function_exists('mb_strlen')) { if (mb_strlen($string) > 40) { $string = mb_substr($string, 0, 30) . '...' . mb_substr($string, -7); } } else { if (strlen($string) > 40) { $string = substr($string, 0, 30) . '...' . substr($string, -7); } } return str_replace("\n", '\n', $string); } if (is_object($value)) { return sprintf( '%s Object (%s)', get_class($value), count($this->toArray($value)) > 0 ? '...' : '' ); } if (is_array($value)) { return sprintf( 'Array (%s)', count($value) > 0 ? '...' : '' ); } return $this->export($value); } /** * Converts an object to an array containing all of its private, protected * and public properties. * * @param mixed $value * @return array */ public function toArray($value) { if (!is_object($value)) { return (array) $value; } $array = array(); foreach ((array) $value as $key => $val) { // properties are transformed to keys in the following way: // private $property => "\0Classname\0property" // protected $property => "\0*\0property" // public $property => "property" if (preg_match('/^\0.+\0(.+)$/', $key, $matches)) { $key = $matches[1]; } // See https://github.com/php/php-src/commit/5721132 if ($key === "\0gcdata") { continue; } $array[$key] = $val; } // Some internal classes like SplObjectStorage don't work with the // above (fast) mechanism nor with reflection in Zend. // Format the output similarly to print_r() in this case if ($value instanceof \SplObjectStorage) { // However, the fast method does work in HHVM, and exposes the // internal implementation. Hide it again. if (property_exists('\SplObjectStorage', '__storage')) { unset($array['__storage']); } elseif (property_exists('\SplObjectStorage', 'storage')) { unset($array['storage']); } if (property_exists('\SplObjectStorage', '__key')) { unset($array['__key']); } foreach ($value as $key => $val) { $array[spl_object_hash($val)] = array( 'obj' => $val, 'inf' => $value->getInfo(), ); } } return $array; } /** * Recursive implementation of export * * @param mixed $value The value to export * @param int $indentation The indentation level of the 2nd+ line * @param \SebastianBergmann\RecursionContext\Context $processed Previously processed objects * @return string * @see SebastianBergmann\Exporter\Exporter::export */ protected function recursiveExport(&$value, $indentation, $processed = null) { if ($value === null) { return 'null'; } if ($value === true) { return 'true'; } if ($value === false) { return 'false'; } if (is_float($value) && floatval(intval($value)) === $value) { return "$value.0"; } if (is_resource($value)) { return sprintf( 'resource(%d) of type (%s)', $value, get_resource_type($value) ); } if (is_string($value)) { // Match for most non printable chars somewhat taking multibyte chars into account if (preg_match('/[^\x09-\x0d\x1b\x20-\xff]/', $value)) { return 'Binary String: 0x' . bin2hex($value); } return "'" . str_replace(array("\r\n", "\n\r", "\r"), array("\n", "\n", "\n"), $value) . "'"; } $whitespace = str_repeat(' ', 4 * $indentation); if (!$processed) { $processed = new Context; } if (is_array($value)) { if (($key = $processed->contains($value)) !== false) { return 'Array &' . $key; } $key = $processed->add($value); $values = ''; if (count($value) > 0) { foreach ($value as $k => $v) { $values .= sprintf( '%s %s => %s' . "\n", $whitespace, $this->recursiveExport($k, $indentation), $this->recursiveExport($value[$k], $indentation + 1, $processed) ); } $values = "\n" . $values . $whitespace; } return sprintf('Array &%s (%s)', $key, $values); } if (is_object($value)) { $class = get_class($value); if ($hash = $processed->contains($value)) { return sprintf('%s Object &%s', $class, $hash); } $hash = $processed->add($value); $values = ''; $array = $this->toArray($value); if (count($array) > 0) { foreach ($array as $k => $v) { $values .= sprintf( '%s %s => %s' . "\n", $whitespace, $this->recursiveExport($k, $indentation), $this->recursiveExport($v, $indentation + 1, $processed) ); } $values = "\n" . $values . $whitespace; } return sprintf('%s Object &%s (%s)', $class, $hash, $values); } return var_export($value, true); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; use ReflectionClass; /** * A blacklist for global state elements that should not be snapshotted. */ class Blacklist { /** * @var array */ private $globalVariables = array(); /** * @var array */ private $classes = array(); /** * @var array */ private $classNamePrefixes = array(); /** * @var array */ private $parentClasses = array(); /** * @var array */ private $interfaces = array(); /** * @var array */ private $staticAttributes = array(); /** * @param string $variableName */ public function addGlobalVariable($variableName) { $this->globalVariables[$variableName] = true; } /** * @param string $className */ public function addClass($className) { $this->classes[] = $className; } /** * @param string $className */ public function addSubclassesOf($className) { $this->parentClasses[] = $className; } /** * @param string $interfaceName */ public function addImplementorsOf($interfaceName) { $this->interfaces[] = $interfaceName; } /** * @param string $classNamePrefix */ public function addClassNamePrefix($classNamePrefix) { $this->classNamePrefixes[] = $classNamePrefix; } /** * @param string $className * @param string $attributeName */ public function addStaticAttribute($className, $attributeName) { if (!isset($this->staticAttributes[$className])) { $this->staticAttributes[$className] = array(); } $this->staticAttributes[$className][$attributeName] = true; } /** * @param string $variableName * @return bool */ public function isGlobalVariableBlacklisted($variableName) { return isset($this->globalVariables[$variableName]); } /** * @param string $className * @param string $attributeName * @return bool */ public function isStaticAttributeBlacklisted($className, $attributeName) { if (in_array($className, $this->classes)) { return true; } foreach ($this->classNamePrefixes as $prefix) { if (strpos($className, $prefix) === 0) { return true; } } $class = new ReflectionClass($className); foreach ($this->parentClasses as $type) { if ($class->isSubclassOf($type)) { return true; } } foreach ($this->interfaces as $type) { if ($class->implementsInterface($type)) { return true; } } if (isset($this->staticAttributes[$className][$attributeName])) { return true; } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; /** * Exports parts of a Snapshot as PHP code. */ class CodeExporter { /** * @param Snapshot $snapshot * @return string */ public function constants(Snapshot $snapshot) { $result = ''; foreach ($snapshot->constants() as $name => $value) { $result .= sprintf( 'if (!defined(\'%s\')) define(\'%s\', %s);' . "\n", $name, $name, $this->exportVariable($value) ); } return $result; } /** * @param Snapshot $snapshot * @return string */ public function iniSettings(Snapshot $snapshot) { $result = ''; foreach ($snapshot->iniSettings() as $key => $value) { $result .= sprintf( '@ini_set(%s, %s);' . "\n", $this->exportVariable($key), $this->exportVariable($value) ); } return $result; } /** * @param mixed $variable * @return string */ private function exportVariable($variable) { if (is_scalar($variable) || is_null($variable) || (is_array($variable) && $this->arrayOnlyContainsScalars($variable))) { return var_export($variable, true); } return 'unserialize(' . var_export(serialize($variable), true) . ')'; } /** * @param array $array * @return bool */ private function arrayOnlyContainsScalars(array $array) { $result = true; foreach ($array as $element) { if (is_array($element)) { $result = self::arrayOnlyContainsScalars($element); } elseif (!is_scalar($element) && !is_null($element)) { $result = false; } if ($result === false) { break; } } return $result; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; /** */ interface Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; use ReflectionProperty; /** * Restorer of snapshots of global state. */ class Restorer { /** * Deletes function definitions that are not defined in a snapshot. * * @param Snapshot $snapshot * @throws RuntimeException when the uopz_delete() function is not available * @see https://github.com/krakjoe/uopz */ public function restoreFunctions(Snapshot $snapshot) { if (!function_exists('uopz_delete')) { throw new RuntimeException('The uopz_delete() function is required for this operation'); } $functions = get_defined_functions(); foreach (array_diff($functions['user'], $snapshot->functions()) as $function) { uopz_delete($function); } } /** * Restores all global and super-global variables from a snapshot. * * @param Snapshot $snapshot */ public function restoreGlobalVariables(Snapshot $snapshot) { $superGlobalArrays = $snapshot->superGlobalArrays(); foreach ($superGlobalArrays as $superGlobalArray) { $this->restoreSuperGlobalArray($snapshot, $superGlobalArray); } $globalVariables = $snapshot->globalVariables(); foreach (array_keys($GLOBALS) as $key) { if ($key != 'GLOBALS' && !in_array($key, $superGlobalArrays) && !$snapshot->blacklist()->isGlobalVariableBlacklisted($key)) { if (isset($globalVariables[$key])) { $GLOBALS[$key] = $globalVariables[$key]; } else { unset($GLOBALS[$key]); } } } } /** * Restores all static attributes in user-defined classes from this snapshot. * * @param Snapshot $snapshot */ public function restoreStaticAttributes(Snapshot $snapshot) { $current = new Snapshot($snapshot->blacklist(), false, false, false, false, true, false, false, false, false); $newClasses = array_diff($current->classes(), $snapshot->classes()); unset($current); foreach ($snapshot->staticAttributes() as $className => $staticAttributes) { foreach ($staticAttributes as $name => $value) { $reflector = new ReflectionProperty($className, $name); $reflector->setAccessible(true); $reflector->setValue($value); } } foreach ($newClasses as $className) { $class = new \ReflectionClass($className); $defaults = $class->getDefaultProperties(); foreach ($class->getProperties() as $attribute) { if (!$attribute->isStatic()) { continue; } $name = $attribute->getName(); if ($snapshot->blacklist()->isStaticAttributeBlacklisted($className, $name)) { continue; } if (!isset($defaults[$name])) { continue; } $attribute->setAccessible(true); $attribute->setValue($defaults[$name]); } } } /** * Restores a super-global variable array from this snapshot. * * @param Snapshot $snapshot * @param $superGlobalArray */ private function restoreSuperGlobalArray(Snapshot $snapshot, $superGlobalArray) { $superGlobalVariables = $snapshot->superGlobalVariables(); if (isset($GLOBALS[$superGlobalArray]) && is_array($GLOBALS[$superGlobalArray]) && isset($superGlobalVariables[$superGlobalArray])) { $keys = array_keys( array_merge( $GLOBALS[$superGlobalArray], $superGlobalVariables[$superGlobalArray] ) ); foreach ($keys as $key) { if (isset($superGlobalVariables[$superGlobalArray][$key])) { $GLOBALS[$superGlobalArray][$key] = $superGlobalVariables[$superGlobalArray][$key]; } else { unset($GLOBALS[$superGlobalArray][$key]); } } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; /** */ class RuntimeException extends \RuntimeException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\GlobalState; use ReflectionClass; use Serializable; /** * A snapshot of global state. */ class Snapshot { /** * @var Blacklist */ private $blacklist; /** * @var array */ private $globalVariables = array(); /** * @var array */ private $superGlobalArrays = array(); /** * @var array */ private $superGlobalVariables = array(); /** * @var array */ private $staticAttributes = array(); /** * @var array */ private $iniSettings = array(); /** * @var array */ private $includedFiles = array(); /** * @var array */ private $constants = array(); /** * @var array */ private $functions = array(); /** * @var array */ private $interfaces = array(); /** * @var array */ private $classes = array(); /** * @var array */ private $traits = array(); /** * Creates a snapshot of the current global state. * * @param Blacklist $blacklist * @param bool $includeGlobalVariables * @param bool $includeStaticAttributes * @param bool $includeConstants * @param bool $includeFunctions * @param bool $includeClasses * @param bool $includeInterfaces * @param bool $includeTraits * @param bool $includeIniSettings * @param bool $includeIncludedFiles */ public function __construct(Blacklist $blacklist = null, $includeGlobalVariables = true, $includeStaticAttributes = true, $includeConstants = true, $includeFunctions = true, $includeClasses = true, $includeInterfaces = true, $includeTraits = true, $includeIniSettings = true, $includeIncludedFiles = true) { if ($blacklist === null) { $blacklist = new Blacklist; } $this->blacklist = $blacklist; if ($includeConstants) { $this->snapshotConstants(); } if ($includeFunctions) { $this->snapshotFunctions(); } if ($includeClasses || $includeStaticAttributes) { $this->snapshotClasses(); } if ($includeInterfaces) { $this->snapshotInterfaces(); } if ($includeGlobalVariables) { $this->setupSuperGlobalArrays(); $this->snapshotGlobals(); } if ($includeStaticAttributes) { $this->snapshotStaticAttributes(); } if ($includeIniSettings) { $this->iniSettings = ini_get_all(null, false); } if ($includeIncludedFiles) { $this->includedFiles = get_included_files(); } if (function_exists('get_declared_traits')) { $this->traits = get_declared_traits(); } } /** * @return Blacklist */ public function blacklist() { return $this->blacklist; } /** * @return array */ public function globalVariables() { return $this->globalVariables; } /** * @return array */ public function superGlobalVariables() { return $this->superGlobalVariables; } /** * Returns a list of all super-global variable arrays. * * @return array */ public function superGlobalArrays() { return $this->superGlobalArrays; } /** * @return array */ public function staticAttributes() { return $this->staticAttributes; } /** * @return array */ public function iniSettings() { return $this->iniSettings; } /** * @return array */ public function includedFiles() { return $this->includedFiles; } /** * @return array */ public function constants() { return $this->constants; } /** * @return array */ public function functions() { return $this->functions; } /** * @return array */ public function interfaces() { return $this->interfaces; } /** * @return array */ public function classes() { return $this->classes; } /** * @return array */ public function traits() { return $this->traits; } /** * Creates a snapshot user-defined constants. */ private function snapshotConstants() { $constants = get_defined_constants(true); if (isset($constants['user'])) { $this->constants = $constants['user']; } } /** * Creates a snapshot user-defined functions. */ private function snapshotFunctions() { $functions = get_defined_functions(); $this->functions = $functions['user']; } /** * Creates a snapshot user-defined classes. */ private function snapshotClasses() { foreach (array_reverse(get_declared_classes()) as $className) { $class = new ReflectionClass($className); if (!$class->isUserDefined()) { break; } $this->classes[] = $className; } $this->classes = array_reverse($this->classes); } /** * Creates a snapshot user-defined interfaces. */ private function snapshotInterfaces() { foreach (array_reverse(get_declared_interfaces()) as $interfaceName) { $class = new ReflectionClass($interfaceName); if (!$class->isUserDefined()) { break; } $this->interfaces[] = $interfaceName; } $this->interfaces = array_reverse($this->interfaces); } /** * Creates a snapshot of all global and super-global variables. */ private function snapshotGlobals() { $superGlobalArrays = $this->superGlobalArrays(); foreach ($superGlobalArrays as $superGlobalArray) { $this->snapshotSuperGlobalArray($superGlobalArray); } foreach (array_keys($GLOBALS) as $key) { if ($key != 'GLOBALS' && !in_array($key, $superGlobalArrays) && $this->canBeSerialized($GLOBALS[$key]) && !$this->blacklist->isGlobalVariableBlacklisted($key)) { $this->globalVariables[$key] = unserialize(serialize($GLOBALS[$key])); } } } /** * Creates a snapshot a super-global variable array. * * @param $superGlobalArray */ private function snapshotSuperGlobalArray($superGlobalArray) { $this->superGlobalVariables[$superGlobalArray] = array(); if (isset($GLOBALS[$superGlobalArray]) && is_array($GLOBALS[$superGlobalArray])) { foreach ($GLOBALS[$superGlobalArray] as $key => $value) { $this->superGlobalVariables[$superGlobalArray][$key] = unserialize(serialize($value)); } } } /** * Creates a snapshot of all static attributes in user-defined classes. */ private function snapshotStaticAttributes() { foreach ($this->classes as $className) { $class = new ReflectionClass($className); $snapshot = array(); foreach ($class->getProperties() as $attribute) { if ($attribute->isStatic()) { $name = $attribute->getName(); if ($this->blacklist->isStaticAttributeBlacklisted($className, $name)) { continue; } $attribute->setAccessible(true); $value = $attribute->getValue(); if ($this->canBeSerialized($value)) { $snapshot[$name] = unserialize(serialize($value)); } } } if (!empty($snapshot)) { $this->staticAttributes[$className] = $snapshot; } } } /** * Returns a list of all super-global variable arrays. * * @return array */ private function setupSuperGlobalArrays() { $this->superGlobalArrays = array( '_ENV', '_POST', '_GET', '_COOKIE', '_SERVER', '_FILES', '_REQUEST' ); if (ini_get('register_long_arrays') == '1') { $this->superGlobalArrays = array_merge( $this->superGlobalArrays, array( 'HTTP_ENV_VARS', 'HTTP_POST_VARS', 'HTTP_GET_VARS', 'HTTP_COOKIE_VARS', 'HTTP_SERVER_VARS', 'HTTP_POST_FILES' ) ); } } /** * @param mixed $variable * @return bool * @todo Implement this properly */ private function canBeSerialized($variable) { if (!is_object($variable)) { return !is_resource($variable); } if ($variable instanceof \stdClass) { return true; } $class = new ReflectionClass($variable); do { if ($class->isInternal()) { return $variable instanceof Serializable; } } while ($class = $class->getParentClass()); return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\RecursionContext; /** * A context containing previously processed arrays and objects * when recursively processing a value. */ final class Context { /** * @var array[] */ private $arrays; /** * @var \SplObjectStorage */ private $objects; /** * Initialises the context */ public function __construct() { $this->arrays = array(); $this->objects = new \SplObjectStorage; } /** * Adds a value to the context. * * @param array|object $value The value to add. * @return int|string The ID of the stored value, either as * a string or integer. * @throws InvalidArgumentException Thrown if $value is not an array or * object */ public function add(&$value) { if (is_array($value)) { return $this->addArray($value); } else if (is_object($value)) { return $this->addObject($value); } throw new InvalidArgumentException( 'Only arrays and objects are supported' ); } /** * Checks if the given value exists within the context. * * @param array|object $value The value to check. * @return int|string|false The string or integer ID of the stored * value if it has already been seen, or * false if the value is not stored. * @throws InvalidArgumentException Thrown if $value is not an array or * object */ public function contains(&$value) { if (is_array($value)) { return $this->containsArray($value); } else if (is_object($value)) { return $this->containsObject($value); } throw new InvalidArgumentException( 'Only arrays and objects are supported' ); } /** * @param array $array * @return bool|int */ private function addArray(array &$array) { $key = $this->containsArray($array); if ($key !== false) { return $key; } $this->arrays[] = &$array; return count($this->arrays) - 1; } /** * @param object $object * @return string */ private function addObject($object) { if (!$this->objects->contains($object)) { $this->objects->attach($object); } return spl_object_hash($object); } /** * @param array $array * @return int|false */ private function containsArray(array &$array) { $keys = array_keys($this->arrays, $array, true); $hash = '_Key_' . microtime(true); foreach ($keys as $key) { $this->arrays[$key][$hash] = $hash; if (isset($array[$hash]) && $array[$hash] === $hash) { unset($this->arrays[$key][$hash]); return $key; } unset($this->arrays[$key][$hash]); } return false; } /** * @param object $value * @return string|false */ private function containsObject($value) { if ($this->objects->contains($value)) { return spl_object_hash($value); } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\RecursionContext; /** */ interface Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann\RecursionContext; /** */ final class InvalidArgumentException extends \InvalidArgumentException implements Exception { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SebastianBergmann; /** * @since Class available since Release 1.0.0 */ class Version { private $path; private $release; private $version; /** * @param string $release * @param string $path */ public function __construct($release, $path) { $this->release = $release; $this->path = $path; } /** * @return string */ public function getVersion() { if ($this->version === null) { if (count(explode('.', $this->release)) == 3) { $this->version = $this->release; } else { $this->version = $this->release . '-dev'; } $git = $this->getGitInformation($this->path); if ($git) { if (count(explode('.', $this->release)) == 3) { $this->version = $git; } else { $git = explode('-', $git); $this->version = $this->release . '-' . end($git); } } } return $this->version; } /** * @param string $path * @return bool|string */ private function getGitInformation($path) { if (!is_dir($path . DIRECTORY_SEPARATOR . '.git')) { return false; } $dir = getcwd(); chdir($path); $returnCode = 1; $result = @exec('git describe --tags 2>&1', $output, $returnCode); chdir($dir); if ($returnCode !== 0) { return false; } return $result; } } * * 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\Descriptor\TextDescriptor; use Symfony\Component\Console\Descriptor\XmlDescriptor; use Symfony\Component\Console\Exception\ExceptionInterface; use Symfony\Component\Console\Helper\DebugFormatterHelper; use Symfony\Component\Console\Helper\ProcessHelper; use Symfony\Component\Console\Helper\QuestionHelper; 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\Input\InputAwareInterface; use Symfony\Component\Console\Output\BufferedOutput; 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; use Symfony\Component\Console\Helper\TableHelper; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleExceptionEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; use Symfony\Component\Console\Exception\CommandNotFoundException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * 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 */ class Application { private $commands = array(); private $wantHelps = false; private $runningCommand; private $name; private $version; private $catchExceptions = true; private $autoExit = true; private $definition; private $helperSet; private $dispatcher; private $terminalDimensions; private $defaultCommand; /** * Constructor. * * @param string $name The name of the application * @param string $version The version of the application */ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') { $this->name = $name; $this->version = $version; $this->defaultCommand = 'list'; $this->helperSet = $this->getDefaultHelperSet(); $this->definition = $this->getDefaultInputDefinition(); foreach ($this->getDefaultCommands() as $command) { $this->add($command); } } public function setDispatcher(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } /** * Runs the current application. * * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * * @return int 0 if everything went fine, or an error code * * @throws \Exception When doRun returns Exception */ public function run(InputInterface $input = null, OutputInterface $output = null) { if (null === $input) { $input = new ArgvInput(); } if (null === $output) { $output = new ConsoleOutput(); } $this->configureIO($input, $output); try { $exitCode = $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); } $exitCode = $e->getCode(); if (is_numeric($exitCode)) { $exitCode = (int) $exitCode; if (0 === $exitCode) { $exitCode = 1; } } else { $exitCode = 1; } } if ($this->autoExit) { if ($exitCode > 255) { $exitCode = 255; } exit($exitCode); } return $exitCode; } /** * Runs the current application. * * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * * @return int 0 if everything went fine, or an error code */ public function doRun(InputInterface $input, OutputInterface $output) { if (true === $input->hasParameterOption(array('--version', '-V'))) { $output->writeln($this->getLongVersion()); return 0; } $name = $this->getCommandName($input); if (true === $input->hasParameterOption(array('--help', '-h'))) { if (!$name) { $name = 'help'; $input = new ArrayInput(array('command' => 'help')); } else { $this->wantHelps = true; } } if (!$name) { $name = $this->defaultCommand; $input = new ArrayInput(array('command' => $this->defaultCommand)); } // the command name MUST be the first element of the input $command = $this->find($name); $this->runningCommand = $command; $exitCode = $this->doRunCommand($command, $input, $output); $this->runningCommand = null; return $exitCode; } /** * Set a helper set to be used with the command. * * @param HelperSet $helperSet The helper set */ 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 */ public function getHelperSet() { return $this->helperSet; } /** * Set an input definition to be used with this application. * * @param InputDefinition $definition The input definition */ 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() { return $this->getLongVersion(); } /** * Sets whether to catch exceptions or not during commands execution. * * @param bool $boolean Whether to catch exceptions or not during commands execution */ public function setCatchExceptions($boolean) { $this->catchExceptions = (bool) $boolean; } /** * Sets whether to automatically exit after a command execution or not. * * @param bool $boolean Whether to automatically exit after a command execution or not */ public function setAutoExit($boolean) { $this->autoExit = (bool) $boolean; } /** * Gets the name of the application. * * @return string The application name */ public function getName() { return $this->name; } /** * Sets the application name. * * @param string $name The application name */ public function setName($name) { $this->name = $name; } /** * Gets the application version. * * @return string The application version */ public function getVersion() { return $this->version; } /** * Sets the application version. * * @param string $version The application version */ public function setVersion($version) { $this->version = $version; } /** * Returns the long version of the application. * * @return string The long application version */ public function getLongVersion() { if ('UNKNOWN' !== $this->getName()) { if ('UNKNOWN' !== $this->getVersion()) { return sprintf('%s version %s', $this->getName(), $this->getVersion()); } return sprintf('%s', $this->getName()); } return 'Console Tool'; } /** * Registers a new command. * * @param string $name The command name * * @return Command The newly created command */ public function register($name) { return $this->add(new Command($name)); } /** * Adds an array of command objects. * * If a Command is not enabled it will not be added. * * @param Command[] $commands An array of commands */ 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. * If the command is not enabled it will not be added. * * @param Command $command A Command object * * @return Command|null The registered command if enabled or null */ public function add(Command $command) { $command->setApplication($this); if (!$command->isEnabled()) { $command->setApplication(null); return; } if (null === $command->getDefinition()) { throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', get_class($command))); } $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 CommandNotFoundException When command name given does not exist */ public function get($name) { if (!isset($this->commands[$name])) { throw new CommandNotFoundException(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 bool true if the command exists, false otherwise */ public function has($name) { return isset($this->commands[$name]); } /** * Returns an array of all unique namespaces used by currently registered commands. * * It does not return the global namespace which always exists. * * @return string[] An array of namespaces */ public function getNamespaces() { $namespaces = array(); foreach ($this->all() as $command) { $namespaces = array_merge($namespaces, $this->extractAllNamespaces($command->getName())); foreach ($command->getAliases() as $alias) { $namespaces = array_merge($namespaces, $this->extractAllNamespaces($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 CommandNotFoundException When namespace is incorrect or ambiguous */ public function findNamespace($namespace) { $allNamespaces = $this->getNamespaces(); $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $namespace); $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces); if (empty($namespaces)) { $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { 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 CommandNotFoundException($message, $alternatives); } $exact = in_array($namespace, $namespaces, true); if (count($namespaces) > 1 && !$exact) { throw new CommandNotFoundException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); } return $exact ? $namespace : reset($namespaces); } /** * 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 CommandNotFoundException When command name is incorrect or ambiguous */ public function find($name) { $allCommands = array_keys($this->commands); $expr = preg_replace_callback('{([^:]+|)}', function ($matches) { return preg_quote($matches[1]).'[^:]*'; }, $name); $commands = preg_grep('{^'.$expr.'}', $allCommands); if (empty($commands) || count(preg_grep('{^'.$expr.'$}', $commands)) < 1) { if (false !== $pos = strrpos($name, ':')) { // check if a namespace exists and contains commands $this->findNamespace(substr($name, 0, $pos)); } $message = sprintf('Command "%s" is not defined.', $name); if ($alternatives = $this->findAlternatives($name, $allCommands)) { 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 CommandNotFoundException($message, $alternatives); } // filter out aliases for commands which are already on the list if (count($commands) > 1) { $commandList = $this->commands; $commands = array_filter($commands, function ($nameOrAlias) use ($commandList, $commands) { $commandName = $commandList[$nameOrAlias]->getName(); return $commandName === $nameOrAlias || !in_array($commandName, $commands); }); } $exact = in_array($name, $commands, true); if (count($commands) > 1 && !$exact) { $suggestions = $this->getAbbreviationSuggestions(array_values($commands)); throw new CommandNotFoundException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions), array_values($commands)); } return $this->get($exact ? $name : reset($commands)); } /** * 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 */ 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); $len > 0; --$len) { $abbrev = substr($name, 0, $len); $abbrevs[$abbrev][] = $name; } } return $abbrevs; } /** * Returns a text representation of the Application. * * @param string $namespace An optional namespace name * @param bool $raw Whether to return raw command list * * @return string A string representing the Application * * @deprecated since version 2.3, to be removed in 3.0. */ public function asText($namespace = null, $raw = false) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); $descriptor = new TextDescriptor(); $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, !$raw); $descriptor->describe($output, $this, array('namespace' => $namespace, 'raw_output' => true)); return $output->fetch(); } /** * Returns an XML representation of the Application. * * @param string $namespace An optional namespace name * @param bool $asDom Whether to return a DOM or an XML string * * @return string|\DOMDocument An XML string representing the Application * * @deprecated since version 2.3, to be removed in 3.0. */ public function asXml($namespace = null, $asDom = false) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); $descriptor = new XmlDescriptor(); if ($asDom) { return $descriptor->getApplicationDocument($this, $namespace); } $output = new BufferedOutput(); $descriptor->describe($output, $this, array('namespace' => $namespace)); return $output->fetch(); } /** * Renders a caught exception. * * @param \Exception $e An exception instance * @param OutputInterface $output An OutputInterface instance */ public function renderException($e, $output) { $output->writeln('', OutputInterface::VERBOSITY_QUIET); do { $title = sprintf(' [%s] ', get_class($e)); $len = $this->stringWidth($title); $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : PHP_INT_MAX; // HHVM only accepts 32 bits integer in str_split, even when PHP_INT_MAX is a 64 bit integer: https://github.com/facebook/hhvm/issues/1327 if (defined('HHVM_VERSION') && $width > 1 << 31) { $width = 1 << 31; } $formatter = $output->getFormatter(); $lines = array(); foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { foreach ($this->splitStringByWidth($line, $width - 4) as $line) { // pre-format lines to get the right string length $lineLength = $this->stringWidth(preg_replace('/\[[^m]*m/', '', $formatter->format($line))) + 4; $lines[] = array($line, $lineLength); $len = max($lineLength, $len); } } $messages = array(); $messages[] = $emptyLine = $formatter->format(sprintf('%s', str_repeat(' ', $len))); $messages[] = $formatter->format(sprintf('%s%s', $title, str_repeat(' ', max(0, $len - $this->stringWidth($title))))); foreach ($lines as $line) { $messages[] = $formatter->format(sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1]))); } $messages[] = $emptyLine; $messages[] = ''; $output->writeln($messages, OutputInterface::OUTPUT_RAW | OutputInterface::VERBOSITY_QUIET); if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { $output->writeln('Exception trace:', OutputInterface::VERBOSITY_QUIET); // 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), OutputInterface::VERBOSITY_QUIET); } $output->writeln('', OutputInterface::VERBOSITY_QUIET); } } while ($e = $e->getPrevious()); if (null !== $this->runningCommand) { $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName())), OutputInterface::VERBOSITY_QUIET); $output->writeln('', OutputInterface::VERBOSITY_QUIET); } } /** * 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 ($this->terminalDimensions) { return $this->terminalDimensions; } if ('\\' === DIRECTORY_SEPARATOR) { // 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); } /** * Sets terminal dimensions. * * Can be useful to force terminal dimensions for functional tests. * * @param int $width The width * @param int $height The height * * @return Application The current application */ public function setTerminalDimensions($width, $height) { $this->terminalDimensions = array($width, $height); return $this; } /** * Configures the input and output instances based on the user arguments and options. * * @param InputInterface $input An InputInterface instance * @param OutputInterface $output An OutputInterface instance */ protected function configureIO(InputInterface $input, OutputInterface $output) { if (true === $input->hasParameterOption(array('--ansi'))) { $output->setDecorated(true); } elseif (true === $input->hasParameterOption(array('--no-ansi'))) { $output->setDecorated(false); } if (true === $input->hasParameterOption(array('--no-interaction', '-n'))) { $input->setInteractive(false); } elseif (function_exists('posix_isatty') && $this->getHelperSet()->has('question')) { $inputStream = $this->getHelperSet()->get('question')->getInputStream(); if (!@posix_isatty($inputStream) && false === getenv('SHELL_INTERACTIVE')) { $input->setInteractive(false); } } if (true === $input->hasParameterOption(array('--quiet', '-q'))) { $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); $input->setInteractive(false); } else { if ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || $input->getParameterOption('--verbose') === 3) { $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || $input->getParameterOption('--verbose') === 2) { $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) { $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); } } } /** * Runs the current command. * * If an event dispatcher has been attached to the application, * events are also dispatched during the life-cycle of the command. * * @param Command $command A Command instance * @param InputInterface $input An Input instance * @param OutputInterface $output An Output instance * * @return int 0 if everything went fine, or an error code * * @throws \Exception when the command being run threw an exception */ protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) { foreach ($command->getHelperSet() as $helper) { if ($helper instanceof InputAwareInterface) { $helper->setInput($input); } } if (null === $this->dispatcher) { try { return $command->run($input, $output); } catch (\Exception $e) { throw $e; } catch (\Throwable $e) { throw new FatalThrowableError($e); } } // bind before the console.command event, so the listeners have access to input options/arguments try { $command->mergeApplicationDefinition(); $input->bind($command->getDefinition()); } catch (ExceptionInterface $e) { // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition } $event = new ConsoleCommandEvent($command, $input, $output); $this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event); if ($event->commandShouldRun()) { try { $e = null; $exitCode = $command->run($input, $output); } catch (\Exception $x) { $e = $x; } catch (\Throwable $x) { $e = new FatalThrowableError($x); } if (null !== $e) { $event = new ConsoleExceptionEvent($command, $input, $output, $e, $e->getCode()); $this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event); if ($e !== $event->getException()) { $x = $e = $event->getException(); } $event = new ConsoleTerminateEvent($command, $input, $output, $e->getCode()); $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event); throw $x; } } else { $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED; } $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode); $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event); return $event->getExitCode(); } /** * 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|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), 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(false), new ProgressHelper(false), new TableHelper(false), new DebugFormatterHelper(), new ProcessHelper(), new QuestionHelper(), )); } /** * 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|null 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]; } } } /** * 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. * * This method is not part of public API and should not be used directly. * * @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 */ public function extractNamespace($name, $limit = null) { $parts = explode(':', $name); array_pop($parts); return implode(':', null === $limit ? $parts : array_slice($parts, 0, $limit)); } /** * 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 * * @return string[] A sorted array of similar string */ private function findAlternatives($name, $collection) { $threshold = 1e3; $alternatives = array(); $collectionParts = array(); foreach ($collection as $item) { $collectionParts[$item] = explode(':', $item); } foreach (explode(':', $name) as $i => $subname) { foreach ($collectionParts as $collectionName => $parts) { $exists = isset($alternatives[$collectionName]); if (!isset($parts[$i]) && $exists) { $alternatives[$collectionName] += $threshold; continue; } elseif (!isset($parts[$i])) { continue; } $lev = levenshtein($subname, $parts[$i]); if ($lev <= strlen($subname) / 3 || '' !== $subname && false !== strpos($parts[$i], $subname)) { $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; } elseif ($exists) { $alternatives[$collectionName] += $threshold; } } } foreach ($collection as $item) { $lev = levenshtein($name, $item); if ($lev <= strlen($name) / 3 || false !== strpos($item, $name)) { $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; } } $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); asort($alternatives); return array_keys($alternatives); } /** * Sets the default Command name. * * @param string $commandName The Command name */ public function setDefaultCommand($commandName) { $this->defaultCommand = $commandName; } private function stringWidth($string) { if (false === $encoding = mb_detect_encoding($string, null, true)) { return strlen($string); } return mb_strwidth($string, $encoding); } private function splitStringByWidth($string, $width) { // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. // additionally, array_slice() is not enough as some character has doubled width. // we need a function to split string not by character count but by string width if (false === $encoding = mb_detect_encoding($string, null, true)) { return str_split($string, $width); } $utf8String = mb_convert_encoding($string, 'utf8', $encoding); $lines = array(); $line = ''; foreach (preg_split('//u', $utf8String) as $char) { // test if $char could be appended to current line if (mb_strwidth($line.$char, 'utf8') <= $width) { $line .= $char; continue; } // if not, push current line to array and make new line $lines[] = str_pad($line, $width); $line = $char; } if ('' !== $line) { $lines[] = count($lines) ? str_pad($line, $width) : $line; } mb_convert_variables($encoding, 'utf8', $lines); return $lines; } /** * Returns all namespaces of the command name. * * @param string $name The full name of the command * * @return string[] The namespaces of the command */ private function extractAllNamespaces($name) { // -1 as third argument is needed to skip the command short name when exploding $parts = explode(':', $name, -1); $namespaces = array(); foreach ($parts as $part) { if (count($namespaces)) { $namespaces[] = end($namespaces).':'.$part; } else { $namespaces[] = $part; } } return $namespaces; } } * * 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\Descriptor\TextDescriptor; use Symfony\Component\Console\Descriptor\XmlDescriptor; use Symfony\Component\Console\Exception\ExceptionInterface; 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\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Base class for all commands. * * @author Fabien Potencier */ class Command { private $application; private $name; private $processTitle; private $aliases = array(); private $definition; private $help; private $description; private $ignoreValidationErrors = false; private $applicationDefinitionMerged = false; private $applicationDefinitionMergedWithArgs = false; private $code; private $synopsis = array(); private $usages = array(); private $helperSet; /** * Constructor. * * @param string|null $name The name of the command; passing null means it must be set in configure() * * @throws LogicException When the command name is empty */ public function __construct($name = null) { $this->definition = new InputDefinition(); if (null !== $name) { $this->setName($name); } $this->configure(); if (!$this->name) { throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_class($this))); } } /** * 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 */ 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 */ 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 bool */ 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|int 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. * * This method is executed before the InputDefinition is validated. * This means that this is the only place where the command can * interactively ask for values of missing required arguments. * * @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 int The command exit code * * @see setCode() * @see execute() */ public function run(InputInterface $input, OutputInterface $output) { // force the creation of the synopsis before the merge with the app definition $this->getSynopsis(true); $this->getSynopsis(false); // add the application arguments and options $this->mergeApplicationDefinition(); // bind the input against the command specific arguments/options try { $input->bind($this->definition); } catch (ExceptionInterface $e) { if (!$this->ignoreValidationErrors) { throw $e; } } $this->initialize($input, $output); if (null !== $this->processTitle) { if (function_exists('cli_set_process_title')) { cli_set_process_title($this->processTitle); } elseif (function_exists('setproctitle')) { setproctitle($this->processTitle); } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) { $output->writeln('Install the proctitle PECL to be able to change the process title.'); } } if ($input->isInteractive()) { $this->interact($input, $output); } // The command name argument is often omitted when a command is executed directly with its run() method. // It would fail the validation if we didn't make sure the command argument is present, // since it's required by the application. if ($input->hasArgument('command') && null === $input->getArgument('command')) { $input->setArgument('command', $this->getName()); } $input->validate(); if ($this->code) { $statusCode = call_user_func($this->code, $input, $output); } else { $statusCode = $this->execute($input, $output); } return is_numeric($statusCode) ? (int) $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() */ public function setCode($code) { if (!is_callable($code)) { throw new InvalidArgumentException('Invalid callable provided to Command::setCode.'); } if (PHP_VERSION_ID >= 50400 && $code instanceof \Closure) { $r = new \ReflectionFunction($code); if (null === $r->getClosureThis()) { $code = \Closure::bind($code, $this); } } $this->code = $code; return $this; } /** * Merges the application definition with the command definition. * * This method is not part of public API and should not be used directly. * * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments */ public function mergeApplicationDefinition($mergeArgs = true) { if (null === $this->application || (true === $this->applicationDefinitionMerged && ($this->applicationDefinitionMergedWithArgs || !$mergeArgs))) { return; } $this->definition->addOptions($this->application->getDefinition()->getOptions()); if ($mergeArgs) { $currentArguments = $this->definition->getArguments(); $this->definition->setArguments($this->application->getDefinition()->getArguments()); $this->definition->addArguments($currentArguments); } $this->applicationDefinitionMerged = true; if ($mergeArgs) { $this->applicationDefinitionMergedWithArgs = 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 */ 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 */ 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. * * This method is not part of public API and should not be used directly. * * @return InputDefinition An InputDefinition instance */ public function getNativeDefinition() { return $this->getDefinition(); } /** * Adds an argument. * * @param string $name The argument name * @param int $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 */ 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 int $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_NONE) * * @return Command The current instance */ 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 the name is invalid */ public function setName($name) { $this->validateName($name); $this->name = $name; return $this; } /** * Sets the process title of the command. * * This feature should be used only when creating a long process command, * like a daemon. * * PHP 5.5+ or the proctitle PECL library is required * * @param string $title The process title * * @return Command The current instance */ public function setProcessTitle($title) { $this->processTitle = $title; return $this; } /** * Returns the command name. * * @return string The command name */ 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 */ public function setDescription($description) { $this->description = $description; return $this; } /** * Returns the description for the command. * * @return string The description for the command */ 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 */ public function setHelp($help) { $this->help = $help; return $this; } /** * Returns the help for the command. * * @return string The help for the command */ 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() ?: $this->getDescription()); } /** * Sets the aliases for the command. * * @param string[] $aliases An array of aliases for the command * * @return Command The current instance * * @throws InvalidArgumentException When an alias is invalid */ public function setAliases($aliases) { if (!is_array($aliases) && !$aliases instanceof \Traversable) { throw new InvalidArgumentException('$aliases must be an array or an instance of \Traversable'); } 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 */ public function getAliases() { return $this->aliases; } /** * Returns the synopsis for the command. * * @param bool $short Whether to show the short version of the synopsis (with options folded) or not * * @return string The synopsis */ public function getSynopsis($short = false) { $key = $short ? 'short' : 'long'; if (!isset($this->synopsis[$key])) { $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); } return $this->synopsis[$key]; } /** * Add a command usage example. * * @param string $usage The usage, it'll be prefixed with the command name * * @return Command The current instance */ public function addUsage($usage) { if (0 !== strpos($usage, $this->name)) { $usage = sprintf('%s %s', $this->name, $usage); } $this->usages[] = $usage; return $this; } /** * Returns alternative usages of the command. * * @return array */ public function getUsages() { return $this->usages; } /** * Gets a helper instance by name. * * @param string $name The helper name * * @return mixed The helper value * * @throws LogicException if no HelperSet is defined * @throws InvalidArgumentException if the helper is not defined */ public function getHelper($name) { if (null === $this->helperSet) { throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name)); } return $this->helperSet->get($name); } /** * Returns a text representation of the command. * * @return string A string representing the command * * @deprecated since version 2.3, to be removed in 3.0. */ public function asText() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); $descriptor = new TextDescriptor(); $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); $descriptor->describe($output, $this, array('raw_output' => true)); return $output->fetch(); } /** * Returns an XML representation of the command. * * @param bool $asDom Whether to return a DOM or an XML string * * @return string|\DOMDocument An XML string representing the command * * @deprecated since version 2.3, to be removed in 3.0. */ public function asXml($asDom = false) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); $descriptor = new XmlDescriptor(); if ($asDom) { return $descriptor->getCommandDocument($this); } $output = new BufferedOutput(); $descriptor->describe($output, $this); return $output->fetch(); } /** * Validates a command name. * * It must be non-empty and parts can optionally be separated by ":". * * @param string $name * * @throws InvalidArgumentException When the name is invalid */ 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\Helper\DescriptorHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * 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'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), )) ->setDescription('Displays help for a command') ->setHelp(<<<'EOF' The %command.name% command displays help for a given command: php %command.full_name% list You can also output the help in other formats by using the --format option: php %command.full_name% --format=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')) { @trigger_error('The --xml option was deprecated in version 2.7 and will be removed in version 3.0. Use the --format option instead.', E_USER_DEPRECATED); $input->setOption('format', 'xml'); } $helper = new DescriptorHelper(); $helper->describe($output, $this->command, array( 'format' => $input->getOption('format'), 'raw_text' => $input->getOption('raw'), )); $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\Helper\DescriptorHelper; 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\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(<<<'EOF' The %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 in other formats by using the --format option: php %command.full_name% --format=xml It's also possible to get raw list of commands (useful for embedding command runner): php %command.full_name% --raw EOF ) ; } /** * {@inheritdoc} */ public function getNativeDefinition() { return $this->createDefinition(); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { if ($input->getOption('xml')) { @trigger_error('The --xml option was deprecated in version 2.7 and will be removed in version 3.0. Use the --format option instead.', E_USER_DEPRECATED); $input->setOption('format', 'xml'); } $helper = new DescriptorHelper(); $helper->describe($output, $this->getApplication(), array( 'format' => $input->getOption('format'), 'raw_text' => $input->getOption('raw'), 'namespace' => $input->getArgument('namespace'), )); } /** * {@inheritdoc} */ private function createDefinition() { return new InputDefinition(array( new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name'), new InputOption('xml', null, InputOption::VALUE_NONE, 'To output list as XML'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), )); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console; /** * Contains all events dispatched by an Application. * * @author Francesco Levorato */ final class ConsoleEvents { /** * The COMMAND event allows you to attach listeners before any command is * executed by the console. It also allows you to modify the command, input and output * before they are handled to the command. * * The event listener method receives a Symfony\Component\Console\Event\ConsoleCommandEvent * instance. * * @Event * * @var string */ const COMMAND = 'console.command'; /** * The TERMINATE event allows you to attach listeners after a command is * executed by the console. * * The event listener method receives a Symfony\Component\Console\Event\ConsoleTerminateEvent * instance. * * @Event * * @var string */ const TERMINATE = 'console.terminate'; /** * The EXCEPTION event occurs when an uncaught exception appears. * * This event allows you to deal with the exception or * to modify the thrown exception. The event listener method receives * a Symfony\Component\Console\Event\ConsoleExceptionEvent * instance. * * @Event * * @var string */ const EXCEPTION = 'console.exception'; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\CommandNotFoundException; /** * @author Jean-François Simon * * @internal */ class ApplicationDescription { const GLOBAL_NAMESPACE = '_global'; /** * @var Application */ private $application; /** * @var null|string */ private $namespace; /** * @var array */ private $namespaces; /** * @var Command[] */ private $commands; /** * @var Command[] */ private $aliases; /** * Constructor. * * @param Application $application * @param string|null $namespace */ public function __construct(Application $application, $namespace = null) { $this->application = $application; $this->namespace = $namespace; } /** * @return array */ public function getNamespaces() { if (null === $this->namespaces) { $this->inspectApplication(); } return $this->namespaces; } /** * @return Command[] */ public function getCommands() { if (null === $this->commands) { $this->inspectApplication(); } return $this->commands; } /** * @param string $name * * @return Command * * @throws CommandNotFoundException */ public function getCommand($name) { if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { throw new CommandNotFoundException(sprintf('Command %s does not exist.', $name)); } return isset($this->commands[$name]) ? $this->commands[$name] : $this->aliases[$name]; } private function inspectApplication() { $this->commands = array(); $this->namespaces = array(); $all = $this->application->all($this->namespace ? $this->application->findNamespace($this->namespace) : null); foreach ($this->sortCommands($all) as $namespace => $commands) { $names = array(); /** @var Command $command */ foreach ($commands as $name => $command) { if (!$command->getName()) { continue; } if ($command->getName() === $name) { $this->commands[$name] = $command; } else { $this->aliases[$name] = $command; } $names[] = $name; } $this->namespaces[$namespace] = array('id' => $namespace, 'commands' => $names); } } /** * @param array $commands * * @return array */ private function sortCommands(array $commands) { $namespacedCommands = array(); $globalCommands = array(); foreach ($commands as $name => $command) { $key = $this->application->extractNamespace($name, 1); if (!$key) { $globalCommands['_global'][$name] = $command; } else { $namespacedCommands[$key][$name] = $command; } } ksort($namespacedCommands); $namespacedCommands = array_merge($globalCommands, $namespacedCommands); foreach ($namespacedCommands as &$commandsSet) { ksort($commandsSet); } // unset reference to keep scope clear unset($commandsSet); return $namespacedCommands; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * @author Jean-François Simon * * @internal */ abstract class Descriptor implements DescriptorInterface { /** * @var OutputInterface */ private $output; /** * {@inheritdoc} */ public function describe(OutputInterface $output, $object, array $options = array()) { $this->output = $output; switch (true) { case $object instanceof InputArgument: $this->describeInputArgument($object, $options); break; case $object instanceof InputOption: $this->describeInputOption($object, $options); break; case $object instanceof InputDefinition: $this->describeInputDefinition($object, $options); break; case $object instanceof Command: $this->describeCommand($object, $options); break; case $object instanceof Application: $this->describeApplication($object, $options); break; default: throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object))); } } /** * Writes content to output. * * @param string $content * @param bool $decorated */ protected function write($content, $decorated = false) { $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW); } /** * Describes an InputArgument instance. * * @param InputArgument $argument * @param array $options * * @return string|mixed */ abstract protected function describeInputArgument(InputArgument $argument, array $options = array()); /** * Describes an InputOption instance. * * @param InputOption $option * @param array $options * * @return string|mixed */ abstract protected function describeInputOption(InputOption $option, array $options = array()); /** * Describes an InputDefinition instance. * * @param InputDefinition $definition * @param array $options * * @return string|mixed */ abstract protected function describeInputDefinition(InputDefinition $definition, array $options = array()); /** * Describes a Command instance. * * @param Command $command * @param array $options * * @return string|mixed */ abstract protected function describeCommand(Command $command, array $options = array()); /** * Describes an Application instance. * * @param Application $application * @param array $options * * @return string|mixed */ abstract protected function describeApplication(Application $application, array $options = array()); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Output\OutputInterface; /** * Descriptor interface. * * @author Jean-François Simon */ interface DescriptorInterface { /** * Describes an InputArgument instance. * * @param OutputInterface $output * @param object $object * @param array $options */ public function describe(OutputInterface $output, $object, array $options = array()); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * JSON descriptor. * * @author Jean-François Simon * * @internal */ class JsonDescriptor extends Descriptor { /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = array()) { $this->writeData($this->getInputArgumentData($argument), $options); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = array()) { $this->writeData($this->getInputOptionData($option), $options); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = array()) { $this->writeData($this->getInputDefinitionData($definition), $options); } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = array()) { $this->writeData($this->getCommandData($command), $options); } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = array()) { $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; $description = new ApplicationDescription($application, $describedNamespace); $commands = array(); foreach ($description->getCommands() as $command) { $commands[] = $this->getCommandData($command); } $data = $describedNamespace ? array('commands' => $commands, 'namespace' => $describedNamespace) : array('commands' => $commands, 'namespaces' => array_values($description->getNamespaces())); $this->writeData($data, $options); } /** * Writes data as json. * * @param array $data * @param array $options * * @return array|string */ private function writeData(array $data, array $options) { $this->write(json_encode($data, isset($options['json_encoding']) ? $options['json_encoding'] : 0)); } /** * @param InputArgument $argument * * @return array */ private function getInputArgumentData(InputArgument $argument) { return array( 'name' => $argument->getName(), 'is_required' => $argument->isRequired(), 'is_array' => $argument->isArray(), 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()), 'default' => $argument->getDefault(), ); } /** * @param InputOption $option * * @return array */ private function getInputOptionData(InputOption $option) { return array( 'name' => '--'.$option->getName(), 'shortcut' => $option->getShortcut() ? '-'.implode('|-', explode('|', $option->getShortcut())) : '', 'accept_value' => $option->acceptValue(), 'is_value_required' => $option->isValueRequired(), 'is_multiple' => $option->isArray(), 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()), 'default' => $option->getDefault(), ); } /** * @param InputDefinition $definition * * @return array */ private function getInputDefinitionData(InputDefinition $definition) { $inputArguments = array(); foreach ($definition->getArguments() as $name => $argument) { $inputArguments[$name] = $this->getInputArgumentData($argument); } $inputOptions = array(); foreach ($definition->getOptions() as $name => $option) { $inputOptions[$name] = $this->getInputOptionData($option); } return array('arguments' => $inputArguments, 'options' => $inputOptions); } /** * @param Command $command * * @return array */ private function getCommandData(Command $command) { $command->getSynopsis(); $command->mergeApplicationDefinition(false); return array( 'name' => $command->getName(), 'usage' => array_merge(array($command->getSynopsis()), $command->getUsages(), $command->getAliases()), 'description' => $command->getDescription(), 'help' => $command->getProcessedHelp(), 'definition' => $this->getInputDefinitionData($command->getNativeDefinition()), ); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * Markdown descriptor. * * @author Jean-François Simon * * @internal */ class MarkdownDescriptor extends Descriptor { /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = array()) { $this->write( '**'.$argument->getName().':**'."\n\n" .'* Name: '.($argument->getName() ?: '')."\n" .'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n" .'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n" .'* Description: '.preg_replace('/\s*[\r\n]\s*/', "\n ", $argument->getDescription() ?: '')."\n" .'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`' ); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = array()) { $this->write( '**'.$option->getName().':**'."\n\n" .'* Name: `--'.$option->getName().'`'."\n" .'* Shortcut: '.($option->getShortcut() ? '`-'.implode('|-', explode('|', $option->getShortcut())).'`' : '')."\n" .'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n" .'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n" .'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n" .'* Description: '.preg_replace('/\s*[\r\n]\s*/', "\n ", $option->getDescription() ?: '')."\n" .'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`' ); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = array()) { if ($showArguments = count($definition->getArguments()) > 0) { $this->write('### Arguments:'); foreach ($definition->getArguments() as $argument) { $this->write("\n\n"); $this->write($this->describeInputArgument($argument)); } } if (count($definition->getOptions()) > 0) { if ($showArguments) { $this->write("\n\n"); } $this->write('### Options:'); foreach ($definition->getOptions() as $option) { $this->write("\n\n"); $this->write($this->describeInputOption($option)); } } } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = array()) { $command->getSynopsis(); $command->mergeApplicationDefinition(false); $this->write( $command->getName()."\n" .str_repeat('-', strlen($command->getName()))."\n\n" .'* Description: '.($command->getDescription() ?: '')."\n" .'* Usage:'."\n\n" .array_reduce(array_merge(array($command->getSynopsis()), $command->getAliases(), $command->getUsages()), function ($carry, $usage) { return $carry.' * `'.$usage.'`'."\n"; }) ); if ($help = $command->getProcessedHelp()) { $this->write("\n"); $this->write($help); } if ($command->getNativeDefinition()) { $this->write("\n\n"); $this->describeInputDefinition($command->getNativeDefinition()); } } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = array()) { $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; $description = new ApplicationDescription($application, $describedNamespace); $this->write($application->getName()."\n".str_repeat('=', strlen($application->getName()))); foreach ($description->getNamespaces() as $namespace) { if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { $this->write("\n\n"); $this->write('**'.$namespace['id'].':**'); } $this->write("\n\n"); $this->write(implode("\n", array_map(function ($commandName) { return '* '.$commandName; }, $namespace['commands']))); } foreach ($description->getCommands() as $command) { $this->write("\n\n"); $this->write($this->describeCommand($command)); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * Text descriptor. * * @author Jean-François Simon * * @internal */ class TextDescriptor extends Descriptor { /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = array()) { if (null !== $argument->getDefault() && (!is_array($argument->getDefault()) || count($argument->getDefault()))) { $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); } else { $default = ''; } $totalWidth = isset($options['total_width']) ? $options['total_width'] : strlen($argument->getName()); $spacingWidth = $totalWidth - strlen($argument->getName()); $this->writeText(sprintf(' %s %s%s%s', $argument->getName(), str_repeat(' ', $spacingWidth), // + 4 = 2 spaces before , 2 spaces after preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()), $default ), $options); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = array()) { if ($option->acceptValue() && null !== $option->getDefault() && (!is_array($option->getDefault()) || count($option->getDefault()))) { $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); } else { $default = ''; } $value = ''; if ($option->acceptValue()) { $value = '='.strtoupper($option->getName()); if ($option->isValueOptional()) { $value = '['.$value.']'; } } $totalWidth = isset($options['total_width']) ? $options['total_width'] : $this->calculateTotalWidthForOptions(array($option)); $synopsis = sprintf('%s%s', $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', sprintf('--%s%s', $option->getName(), $value) ); $spacingWidth = $totalWidth - strlen($synopsis); $this->writeText(sprintf(' %s %s%s%s%s', $synopsis, str_repeat(' ', $spacingWidth), // + 4 = 2 spaces before , 2 spaces after preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()), $default, $option->isArray() ? ' (multiple values allowed)' : '' ), $options); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = array()) { $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); foreach ($definition->getArguments() as $argument) { $totalWidth = max($totalWidth, strlen($argument->getName())); } if ($definition->getArguments()) { $this->writeText('Arguments:', $options); $this->writeText("\n"); foreach ($definition->getArguments() as $argument) { $this->describeInputArgument($argument, array_merge($options, array('total_width' => $totalWidth))); $this->writeText("\n"); } } if ($definition->getArguments() && $definition->getOptions()) { $this->writeText("\n"); } if ($definition->getOptions()) { $laterOptions = array(); $this->writeText('Options:', $options); foreach ($definition->getOptions() as $option) { if (strlen($option->getShortcut()) > 1) { $laterOptions[] = $option; continue; } $this->writeText("\n"); $this->describeInputOption($option, array_merge($options, array('total_width' => $totalWidth))); } foreach ($laterOptions as $option) { $this->writeText("\n"); $this->describeInputOption($option, array_merge($options, array('total_width' => $totalWidth))); } } } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = array()) { $command->getSynopsis(true); $command->getSynopsis(false); $command->mergeApplicationDefinition(false); $this->writeText('Usage:', $options); foreach (array_merge(array($command->getSynopsis(true)), $command->getAliases(), $command->getUsages()) as $usage) { $this->writeText("\n"); $this->writeText(' '.$usage, $options); } $this->writeText("\n"); $definition = $command->getNativeDefinition(); if ($definition->getOptions() || $definition->getArguments()) { $this->writeText("\n"); $this->describeInputDefinition($definition, $options); $this->writeText("\n"); } if ($help = $command->getProcessedHelp()) { $this->writeText("\n"); $this->writeText('Help:', $options); $this->writeText("\n"); $this->writeText(' '.str_replace("\n", "\n ", $help), $options); $this->writeText("\n"); } } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = array()) { $describedNamespace = isset($options['namespace']) ? $options['namespace'] : null; $description = new ApplicationDescription($application, $describedNamespace); if (isset($options['raw_text']) && $options['raw_text']) { $width = $this->getColumnWidth($description->getCommands()); foreach ($description->getCommands() as $command) { $this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options); $this->writeText("\n"); } } else { if ('' != $help = $application->getHelp()) { $this->writeText("$help\n\n", $options); } $this->writeText("Usage:\n", $options); $this->writeText(" command [options] [arguments]\n\n", $options); $this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options); $this->writeText("\n"); $this->writeText("\n"); $width = $this->getColumnWidth($description->getCommands()); if ($describedNamespace) { $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); } else { $this->writeText('Available commands:', $options); } // add commands by namespace foreach ($description->getNamespaces() as $namespace) { if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { $this->writeText("\n"); $this->writeText(' '.$namespace['id'].'', $options); } foreach ($namespace['commands'] as $name) { $this->writeText("\n"); $spacingWidth = $width - strlen($name); $this->writeText(sprintf(' %s%s%s', $name, str_repeat(' ', $spacingWidth), $description->getCommand($name)->getDescription()), $options); } } $this->writeText("\n"); } } /** * {@inheritdoc} */ private function writeText($content, array $options = array()) { $this->write( isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content, isset($options['raw_output']) ? !$options['raw_output'] : true ); } /** * Formats input option/argument default value. * * @param mixed $default * * @return string */ private function formatDefaultValue($default) { if (PHP_VERSION_ID < 50400) { return str_replace(array('\/', '\\\\'), array('/', '\\'), json_encode($default)); } return str_replace('\\\\', '\\', json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } /** * @param Command[] $commands * * @return int */ private function getColumnWidth(array $commands) { $widths = array(); foreach ($commands as $command) { $widths[] = strlen($command->getName()); foreach ($command->getAliases() as $alias) { $widths[] = strlen($alias); } } return max($widths) + 2; } /** * @param InputOption[] $options * * @return int */ private function calculateTotalWidthForOptions($options) { $totalWidth = 0; foreach ($options as $option) { // "-" + shortcut + ", --" + name $nameLength = 1 + max(strlen($option->getShortcut()), 1) + 4 + strlen($option->getName()); if ($option->acceptValue()) { $valueLength = 1 + strlen($option->getName()); // = + value $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] $nameLength += $valueLength; } $totalWidth = max($totalWidth, $nameLength); } return $totalWidth; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Descriptor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputOption; /** * XML descriptor. * * @author Jean-François Simon * * @internal */ class XmlDescriptor extends Descriptor { /** * @param InputDefinition $definition * * @return \DOMDocument */ public function getInputDefinitionDocument(InputDefinition $definition) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($definitionXML = $dom->createElement('definition')); $definitionXML->appendChild($argumentsXML = $dom->createElement('arguments')); foreach ($definition->getArguments() as $argument) { $this->appendDocument($argumentsXML, $this->getInputArgumentDocument($argument)); } $definitionXML->appendChild($optionsXML = $dom->createElement('options')); foreach ($definition->getOptions() as $option) { $this->appendDocument($optionsXML, $this->getInputOptionDocument($option)); } return $dom; } /** * @param Command $command * * @return \DOMDocument */ public function getCommandDocument(Command $command) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($commandXML = $dom->createElement('command')); $command->getSynopsis(); $command->mergeApplicationDefinition(false); $commandXML->setAttribute('id', $command->getName()); $commandXML->setAttribute('name', $command->getName()); $commandXML->appendChild($usagesXML = $dom->createElement('usages')); foreach (array_merge(array($command->getSynopsis()), $command->getAliases(), $command->getUsages()) as $usage) { $usagesXML->appendChild($dom->createElement('usage', $usage)); } $commandXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription()))); $commandXML->appendChild($helpXML = $dom->createElement('help')); $helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp()))); $definitionXML = $this->getInputDefinitionDocument($command->getNativeDefinition()); $this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0)); return $dom; } /** * @param Application $application * @param string|null $namespace * * @return \DOMDocument */ public function getApplicationDocument(Application $application, $namespace = null) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($rootXml = $dom->createElement('symfony')); if ($application->getName() !== 'UNKNOWN') { $rootXml->setAttribute('name', $application->getName()); if ($application->getVersion() !== 'UNKNOWN') { $rootXml->setAttribute('version', $application->getVersion()); } } $rootXml->appendChild($commandsXML = $dom->createElement('commands')); $description = new ApplicationDescription($application, $namespace); if ($namespace) { $commandsXML->setAttribute('namespace', $namespace); } foreach ($description->getCommands() as $command) { $this->appendDocument($commandsXML, $this->getCommandDocument($command)); } if (!$namespace) { $rootXml->appendChild($namespacesXML = $dom->createElement('namespaces')); foreach ($description->getNamespaces() as $namespaceDescription) { $namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace')); $namespaceArrayXML->setAttribute('id', $namespaceDescription['id']); foreach ($namespaceDescription['commands'] as $name) { $namespaceArrayXML->appendChild($commandXML = $dom->createElement('command')); $commandXML->appendChild($dom->createTextNode($name)); } } } return $dom; } /** * {@inheritdoc} */ protected function describeInputArgument(InputArgument $argument, array $options = array()) { $this->writeDocument($this->getInputArgumentDocument($argument)); } /** * {@inheritdoc} */ protected function describeInputOption(InputOption $option, array $options = array()) { $this->writeDocument($this->getInputOptionDocument($option)); } /** * {@inheritdoc} */ protected function describeInputDefinition(InputDefinition $definition, array $options = array()) { $this->writeDocument($this->getInputDefinitionDocument($definition)); } /** * {@inheritdoc} */ protected function describeCommand(Command $command, array $options = array()) { $this->writeDocument($this->getCommandDocument($command)); } /** * {@inheritdoc} */ protected function describeApplication(Application $application, array $options = array()) { $this->writeDocument($this->getApplicationDocument($application, isset($options['namespace']) ? $options['namespace'] : null)); } /** * Appends document children to parent node. * * @param \DOMNode $parentNode * @param \DOMNode $importedParent */ private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent) { foreach ($importedParent->childNodes as $childNode) { $parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true)); } } /** * Writes DOM document. * * @param \DOMDocument $dom * * @return \DOMDocument|string */ private function writeDocument(\DOMDocument $dom) { $dom->formatOutput = true; $this->write($dom->saveXML()); } /** * @param InputArgument $argument * * @return \DOMDocument */ private function getInputArgumentDocument(InputArgument $argument) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($objectXML = $dom->createElement('argument')); $objectXML->setAttribute('name', $argument->getName()); $objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0); $objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0); $objectXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode($argument->getDescription())); $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); $defaults = is_array($argument->getDefault()) ? $argument->getDefault() : (is_bool($argument->getDefault()) ? array(var_export($argument->getDefault(), true)) : ($argument->getDefault() ? array($argument->getDefault()) : array())); foreach ($defaults as $default) { $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); $defaultXML->appendChild($dom->createTextNode($default)); } return $dom; } /** * @param InputOption $option * * @return \DOMDocument */ private function getInputOptionDocument(InputOption $option) { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($objectXML = $dom->createElement('option')); $objectXML->setAttribute('name', '--'.$option->getName()); $pos = strpos($option->getShortcut(), '|'); if (false !== $pos) { $objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos)); $objectXML->setAttribute('shortcuts', '-'.implode('|-', explode('|', $option->getShortcut()))); } else { $objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : ''); } $objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0); $objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0); $objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0); $objectXML->appendChild($descriptionXML = $dom->createElement('description')); $descriptionXML->appendChild($dom->createTextNode($option->getDescription())); if ($option->acceptValue()) { $defaults = is_array($option->getDefault()) ? $option->getDefault() : (is_bool($option->getDefault()) ? array(var_export($option->getDefault(), true)) : ($option->getDefault() ? array($option->getDefault()) : array())); $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); if (!empty($defaults)) { foreach ($defaults as $default) { $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); $defaultXML->appendChild($dom->createTextNode($default)); } } } return $dom; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; /** * Allows to do things before the command is executed, like skipping the command or changing the input. * * @author Fabien Potencier */ class ConsoleCommandEvent extends ConsoleEvent { /** * The return code for skipped commands, this will also be passed into the terminate event. */ const RETURN_CODE_DISABLED = 113; /** * Indicates if the command should be run or skipped. * * @var bool */ private $commandShouldRun = true; /** * Disables the command, so it won't be run. * * @return bool */ public function disableCommand() { return $this->commandShouldRun = false; } /** * Enables the command. * * @return bool */ public function enableCommand() { return $this->commandShouldRun = true; } /** * Returns true if the command is runnable, false otherwise. * * @return bool */ public function commandShouldRun() { return $this->commandShouldRun; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\Event; /** * Allows to inspect input and output of a command. * * @author Francesco Levorato */ class ConsoleEvent extends Event { protected $command; private $input; private $output; public function __construct(Command $command, InputInterface $input, OutputInterface $output) { $this->command = $command; $this->input = $input; $this->output = $output; } /** * Gets the command that is executed. * * @return Command A Command instance */ public function getCommand() { return $this->command; } /** * Gets the input instance. * * @return InputInterface An InputInterface instance */ public function getInput() { return $this->input; } /** * Gets the output instance. * * @return OutputInterface An OutputInterface instance */ public function getOutput() { return $this->output; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Allows to handle exception thrown in a command. * * @author Fabien Potencier */ class ConsoleExceptionEvent extends ConsoleEvent { private $exception; private $exitCode; public function __construct(Command $command, InputInterface $input, OutputInterface $output, \Exception $exception, $exitCode) { parent::__construct($command, $input, $output); $this->setException($exception); $this->exitCode = (int) $exitCode; } /** * Returns the thrown exception. * * @return \Exception The thrown exception */ public function getException() { return $this->exception; } /** * Replaces the thrown exception. * * This exception will be thrown if no response is set in the event. * * @param \Exception $exception The thrown exception */ public function setException(\Exception $exception) { $this->exception = $exception; } /** * Gets the exit code. * * @return int The command exit code */ public function getExitCode() { return $this->exitCode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Event; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; /** * Allows to manipulate the exit code of a command after its execution. * * @author Francesco Levorato */ class ConsoleTerminateEvent extends ConsoleEvent { /** * The exit code of the command. * * @var int */ private $exitCode; public function __construct(Command $command, InputInterface $input, OutputInterface $output, $exitCode) { parent::__construct($command, $input, $output); $this->setExitCode($exitCode); } /** * Sets the exit code. * * @param int $exitCode The command exit code */ public function setExitCode($exitCode) { $this->exitCode = (int) $exitCode; } /** * Gets the exit code. * * @return int The command exit code */ public function getExitCode() { return $this->exitCode; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * Represents an incorrect command name typed in the console. * * @author Jérôme Tamarelle */ class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface { private $alternatives; /** * @param string $message Exception message to throw * @param array $alternatives List of similar defined names * @param int $code Exception code * @param Exception $previous previous exception used for the exception chaining */ public function __construct($message, array $alternatives = array(), $code = 0, \Exception $previous = null) { parent::__construct($message, $code, $previous); $this->alternatives = $alternatives; } /** * @return array A list of similar defined names */ public function getAlternatives() { return $this->alternatives; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * ExceptionInterface. * * @author Jérôme Tamarelle */ interface ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * @author Jérôme Tamarelle */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * Represents an incorrect option name typed in the console. * * @author Jérôme Tamarelle */ class InvalidOptionException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * @author Jérôme Tamarelle */ class LogicException extends \LogicException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Exception; /** * @author Jérôme Tamarelle */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Formatter; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * Formatter class for console output. * * @author Konstantin Kudryashov */ class OutputFormatter implements OutputFormatterInterface { 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) { $text = preg_replace('/([^\\\\]?) FormatterStyle" instances */ public function __construct($decorated = false, array $styles = array()) { $this->decorated = (bool) $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 bool $decorated Whether to decorate the messages or not */ public function setDecorated($decorated) { $this->decorated = (bool) $decorated; } /** * Gets the decorated flag. * * @return bool true if the output will decorate messages, false otherwise */ public function isDecorated() { return $this->decorated; } /** * Sets a new style. * * @param string $name The style name * @param OutputFormatterStyleInterface $style The style instance */ public function setStyle($name, OutputFormatterStyleInterface $style) { $this->styles[strtolower($name)] = $style; } /** * Checks if output formatter has style with specified name. * * @param string $name * * @return bool */ 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 */ public function getStyle($name) { if (!$this->hasStyle($name)) { throw new InvalidArgumentException(sprintf('Undefined style: %s', $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 */ public function format($message) { $message = (string) $message; $offset = 0; $output = ''; $tagRegex = '[a-z][a-z0-9_=;-]*+'; preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#ix", $message, $matches, PREG_OFFSET_CAPTURE); foreach ($matches[0] as $i => $match) { $pos = $match[1]; $text = $match[0]; if (0 != $pos && '\\' == $message[$pos - 1]) { continue; } // add the text up to the next tag $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset)); $offset = $pos + strlen($text); // opening tag? if ($open = '/' != $text[1]) { $tag = $matches[1][$i][0]; } else { $tag = isset($matches[3][$i][0]) ? $matches[3][$i][0] : ''; } if (!$open && !$tag) { // $this->styleStack->pop(); } elseif (false === $style = $this->createStyleFromString(strtolower($tag))) { $output .= $this->applyCurrentStyle($text); } elseif ($open) { $this->styleStack->push($style); } else { $this->styleStack->pop($style); } } $output .= $this->applyCurrentStyle(substr($message, $offset)); if (false !== strpos($output, '<<')) { return strtr($output, array('\\<' => '<', '<<' => '\\')); } return str_replace('\\<', '<', $output); } /** * @return OutputFormatterStyleStack */ public function getStyleStack() { return $this->styleStack; } /** * Tries to create new style instance from string. * * @param string $string * * @return OutputFormatterStyle|bool false if string is not format string */ private function createStyleFromString($string) { if (isset($this->styles[$string])) { return $this->styles[$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 { try { $style->setOption($match[1]); } catch (\InvalidArgumentException $e) { return false; } } } return $style; } /** * Applies current style from stack to text, if must be applied. * * @param string $text Input text * * @return 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 */ interface OutputFormatterInterface { /** * Sets the decorated flag. * * @param bool $decorated Whether to decorate the messages or not */ public function setDecorated($decorated); /** * Gets the decorated flag. * * @return bool true if the output will decorate messages, false otherwise */ public function isDecorated(); /** * Sets a new style. * * @param string $name The style name * @param OutputFormatterStyleInterface $style The style instance */ public function setStyle($name, OutputFormatterStyleInterface $style); /** * Checks if output formatter has style with specified name. * * @param string $name * * @return bool */ public function hasStyle($name); /** * Gets style options from style with specified name. * * @param string $name * * @return OutputFormatterStyleInterface */ public function getStyle($name); /** * Formats a message according to the given styles. * * @param string $message The message to style * * @return string The styled message */ 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; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * Formatter style class for defining styles. * * @author Konstantin Kudryashov */ class OutputFormatterStyle implements OutputFormatterStyleInterface { private static $availableForegroundColors = array( 'black' => array('set' => 30, 'unset' => 39), 'red' => array('set' => 31, 'unset' => 39), 'green' => array('set' => 32, 'unset' => 39), 'yellow' => array('set' => 33, 'unset' => 39), 'blue' => array('set' => 34, 'unset' => 39), 'magenta' => array('set' => 35, 'unset' => 39), 'cyan' => array('set' => 36, 'unset' => 39), 'white' => array('set' => 37, 'unset' => 39), 'default' => array('set' => 39, 'unset' => 39), ); private static $availableBackgroundColors = array( 'black' => array('set' => 40, 'unset' => 49), 'red' => array('set' => 41, 'unset' => 49), 'green' => array('set' => 42, 'unset' => 49), 'yellow' => array('set' => 43, 'unset' => 49), 'blue' => array('set' => 44, 'unset' => 49), 'magenta' => array('set' => 45, 'unset' => 49), 'cyan' => array('set' => 46, 'unset' => 49), 'white' => array('set' => 47, 'unset' => 49), 'default' => array('set' => 49, 'unset' => 49), ); private static $availableOptions = array( 'bold' => array('set' => 1, 'unset' => 22), 'underscore' => array('set' => 4, 'unset' => 24), 'blink' => array('set' => 5, 'unset' => 25), 'reverse' => array('set' => 7, 'unset' => 27), 'conceal' => array('set' => 8, 'unset' => 28), ); private $foreground; private $background; private $options = array(); /** * Initializes output formatter style. * * @param string|null $foreground The style foreground color name * @param string|null $background The style background color name * @param array $options The style options */ 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|null $color The color name * * @throws InvalidArgumentException When the color name isn't defined */ 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|null $color The color name * * @throws InvalidArgumentException When the color name isn't defined */ 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 */ 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 (!in_array(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) { $setCodes = array(); $unsetCodes = array(); if (null !== $this->foreground) { $setCodes[] = $this->foreground['set']; $unsetCodes[] = $this->foreground['unset']; } if (null !== $this->background) { $setCodes[] = $this->background['set']; $unsetCodes[] = $this->background['unset']; } if (count($this->options)) { foreach ($this->options as $option) { $setCodes[] = $option['set']; $unsetCodes[] = $option['unset']; } } if (0 === count($setCodes)) { return $text; } return sprintf("\033[%sm%s\033[%sm", implode(';', $setCodes), $text, implode(';', $unsetCodes)); } } * * 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 */ interface OutputFormatterStyleInterface { /** * Sets style foreground color. * * @param string $color The color name */ public function setForeground($color = null); /** * Sets style background color. * * @param string $color The color name */ public function setBackground($color = null); /** * Sets some specific style option. * * @param string $option The option name */ 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; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * @author Jean-François Simon */ class OutputFormatterStyleStack { /** * @var OutputFormatterStyleInterface[] */ private $styles; /** * @var OutputFormatterStyleInterface */ private $emptyStyle; /** * Constructor. * * @param OutputFormatterStyleInterface|null $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|null $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; /** * Helps outputting debug information when running an external program from a command. * * An external program can be a Process, an HTTP request, or anything else. * * @author Fabien Potencier */ class DebugFormatterHelper extends Helper { private $colors = array('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default'); private $started = array(); private $count = -1; /** * Starts a debug formatting session. * * @param string $id The id of the formatting session * @param string $message The message to display * @param string $prefix The prefix to use * * @return string */ public function start($id, $message, $prefix = 'RUN') { $this->started[$id] = array('border' => ++$this->count % count($this->colors)); return sprintf("%s %s %s\n", $this->getBorder($id), $prefix, $message); } /** * Adds progress to a formatting session. * * @param string $id The id of the formatting session * @param string $buffer The message to display * @param bool $error Whether to consider the buffer as error * @param string $prefix The prefix for output * @param string $errorPrefix The prefix for error output * * @return string */ public function progress($id, $buffer, $error = false, $prefix = 'OUT', $errorPrefix = 'ERR') { $message = ''; if ($error) { if (isset($this->started[$id]['out'])) { $message .= "\n"; unset($this->started[$id]['out']); } if (!isset($this->started[$id]['err'])) { $message .= sprintf('%s %s ', $this->getBorder($id), $errorPrefix); $this->started[$id]['err'] = true; } $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $errorPrefix), $buffer); } else { if (isset($this->started[$id]['err'])) { $message .= "\n"; unset($this->started[$id]['err']); } if (!isset($this->started[$id]['out'])) { $message .= sprintf('%s %s ', $this->getBorder($id), $prefix); $this->started[$id]['out'] = true; } $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $prefix), $buffer); } return $message; } /** * Stops a formatting session. * * @param string $id The id of the formatting session * @param string $message The message to display * @param bool $successful Whether to consider the result as success * @param string $prefix The prefix for the end output * * @return string */ public function stop($id, $message, $successful, $prefix = 'RES') { $trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : ''; if ($successful) { return sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); } $message = sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); unset($this->started[$id]['out'], $this->started[$id]['err']); return $message; } /** * @param string $id The id of the formatting session * * @return string */ private function getBorder($id) { return sprintf(' ', $this->colors[$this->started[$id]['border']]); } /** * {@inheritdoc} */ public function getName() { return 'debug_formatter'; } } * * 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\Descriptor\DescriptorInterface; use Symfony\Component\Console\Descriptor\JsonDescriptor; use Symfony\Component\Console\Descriptor\MarkdownDescriptor; use Symfony\Component\Console\Descriptor\TextDescriptor; use Symfony\Component\Console\Descriptor\XmlDescriptor; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * This class adds helper method to describe objects in various formats. * * @author Jean-François Simon */ class DescriptorHelper extends Helper { /** * @var DescriptorInterface[] */ private $descriptors = array(); /** * Constructor. */ public function __construct() { $this ->register('txt', new TextDescriptor()) ->register('xml', new XmlDescriptor()) ->register('json', new JsonDescriptor()) ->register('md', new MarkdownDescriptor()) ; } /** * Describes an object if supported. * * Available options are: * * format: string, the output format name * * raw_text: boolean, sets output type as raw * * @param OutputInterface $output * @param object $object * @param array $options * * @throws InvalidArgumentException when the given format is not supported */ public function describe(OutputInterface $output, $object, array $options = array()) { $options = array_merge(array( 'raw_text' => false, 'format' => 'txt', ), $options); if (!isset($this->descriptors[$options['format']])) { throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $options['format'])); } $descriptor = $this->descriptors[$options['format']]; $descriptor->describe($output, $object, $options); } /** * Registers a descriptor. * * @param string $format * @param DescriptorInterface $descriptor * * @return DescriptorHelper */ public function register($format, DescriptorInterface $descriptor) { $this->descriptors[$format] = $descriptor; return $this; } /** * {@inheritdoc} */ public function getName() { return 'descriptor'; } } * * 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\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Output\ConsoleOutputInterface; 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 * * @deprecated since version 2.5, to be removed in 3.0. * Use {@link \Symfony\Component\Console\Helper\QuestionHelper} instead. */ class DialogHelper extends InputAwareHelper { private $inputStream; private static $shell; private static $stty; public function __construct($triggerDeprecationError = true) { if ($triggerDeprecationError) { @trigger_error('"Symfony\Component\Console\Helper\DialogHelper" is deprecated since version 2.5 and will be removed in 3.0. Use "Symfony\Component\Console\Helper\QuestionHelper" instead.', E_USER_DEPRECATED); } } /** * 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 bool|string $default The default answer if the user enters nothing * @param bool|int $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 * @param bool $multiselect Select more than one value separated by comma * * @return int|string|array The selected value or values (the key of the choices array) * * @throws InvalidArgumentException */ public function select(OutputInterface $output, $question, $choices, $default = null, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $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, $multiselect) { // Collapse all spaces. $selectedChoices = str_replace(' ', '', $picked); if ($multiselect) { // Check for a separated comma values if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) { throw new InvalidArgumentException(sprintf($errorMessage, $picked)); } $selectedChoices = explode(',', $selectedChoices); } else { $selectedChoices = array($picked); } $multiselectChoices = array(); foreach ($selectedChoices as $value) { if (empty($choices[$value])) { throw new InvalidArgumentException(sprintf($errorMessage, $value)); } $multiselectChoices[] = $value; } if ($multiselect) { return $multiselectChoices; } 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) { if ($this->input && !$this->input->isInteractive()) { return $default; } if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $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 (!feof($inputStream)) { $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 (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { if ('A' === $c[2] && -1 === $ofs) { $ofs = 0; } if (0 === $numMatches) { continue; } $ofs += ('A' === $c[2]) ? -1 : 1; $ofs = ($numMatches + $ofs) % $numMatches; } } elseif (ord($c) < 32) { if ("\t" === $c || "\n" === $c) { if ($numMatches > 0 && -1 !== $ofs) { $ret = $matches[$ofs]; // Echo out remaining chars for current match $output->write(substr($ret, $i)); $i = strlen($ret); } if ("\n" === $c) { $output->write($c); break; } $numMatches = 0; } continue; } else { $output->write($c); $ret .= $c; ++$i; $numMatches = 0; $ofs = 0; foreach ($autocomplete as $value) { // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) if (0 === strpos($value, $ret) && $i !== strlen($value)) { $matches[$numMatches++] = $value; } } } // Erase characters from cursor to end of line $output->write("\033[K"); if ($numMatches > 0 && -1 !== $ofs) { // Save cursor position $output->write("\0337"); // Write highlighted text $output->write(''.substr($matches[$ofs], $i).''); // Restore cursor position $output->write("\0338"); } } // Reset stty so it behaves normally again shell_exec(sprintf('stty %s', $sttyMode)); } return strlen($ret) > 0 ? $ret : $default; } /** * Asks a confirmation to the user. * * The question will be asked until the user answers by nothing, yes, or no. * * @param OutputInterface $output An Output instance * @param string|array $question The question to ask * @param bool $default The default answer if the user enters nothing * * @return bool true if the user has confirmed, false otherwise */ public function askConfirmation(OutputInterface $output, $question, $default = true) { $answer = 'z'; while ($answer && !in_array(strtolower($answer[0]), array('y', 'n'))) { $answer = $this->ask($output, $question); } if (false === $default) { return $answer && 'y' == strtolower($answer[0]); } return !$answer || 'y' == strtolower($answer[0]); } /** * Asks a question to the user, the response is hidden. * * @param OutputInterface $output An Output instance * @param string|array $question The question * @param bool $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not * * @return string The answer * * @throws RuntimeException In case the fallback is deactivated and the response can not be hidden */ public function askHiddenResponse(OutputInterface $output, $question, $fallback = true) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } if ('\\' === DIRECTORY_SEPARATOR) { $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; // handle code running from a phar if ('phar:' === substr(__FILE__, 0, 5)) { $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; copy($exe, $tmpExe); $exe = $tmpExe; } $output->write($question); $value = rtrim(shell_exec($exe)); $output->writeln(''); if (isset($tmpExe)) { unlink($tmpExe); } return $value; } if ($this->hasSttyAvailable()) { $output->write($question); $sttyMode = shell_exec('stty -g'); shell_exec('stty -echo'); $value = fgets($this->inputStream ?: STDIN, 4096); shell_exec(sprintf('stty %s', $sttyMode)); if (false === $value) { throw new RuntimeException('Aborted'); } $value = trim($value); $output->writeln(''); return $value; } if (false !== $shell = $this->getShell()) { $output->write($question); $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); $value = rtrim(shell_exec($command)); $output->writeln(''); return $value; } if ($fallback) { return $this->ask($output, $question); } throw new RuntimeException('Unable to hide the response'); } /** * Asks for a value and validates the response. * * The validator receives the data to validate. It must return the * validated data when the data is valid and throw an exception * otherwise. * * @param OutputInterface $output An Output instance * @param string|array $question The question to ask * @param callable $validator A PHP callback * @param int|false $attempts Max number of times to ask before giving up (false by default, which means infinite) * @param string $default The default answer if none is given by the user * @param array $autocomplete List of values to autocomplete * * @return mixed * * @throws \Exception When any of the validators return an error */ public function askAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $default = null, array $autocomplete = null) { $that = $this; $interviewer = function () use ($output, $question, $default, $autocomplete, $that) { return $that->ask($output, $question, $default, $autocomplete); }; return $this->validateAttempts($interviewer, $output, $validator, $attempts); } /** * Asks for a value, hide and validates the response. * * The validator receives the data to validate. It must return the * validated data when the data is valid and throw an exception * otherwise. * * @param OutputInterface $output An Output instance * @param string|array $question The question to ask * @param callable $validator A PHP callback * @param int|false $attempts Max number of times to ask before giving up (false by default, which means infinite) * @param bool $fallback In case the response can not be hidden, whether to fallback on non-hidden question or not * * @return string The response * * @throws \Exception When any of the validators return an error * @throws RuntimeException In case the fallback is deactivated and the response can not be hidden */ public function askHiddenResponseAndValidate(OutputInterface $output, $question, $validator, $attempts = false, $fallback = true) { $that = $this; $interviewer = function () use ($output, $question, $fallback, $that) { return $that->askHiddenResponse($output, $question, $fallback); }; return $this->validateAttempts($interviewer, $output, $validator, $attempts); } /** * Sets the input stream to read from when interacting with the user. * * This is mainly useful for testing purpose. * * @param resource $stream The input stream */ public function setInputStream($stream) { $this->inputStream = $stream; } /** * Returns the helper's input stream. * * @return resource|null The input stream or null if the default STDIN is used */ public function getInputStream() { return $this->inputStream; } /** * {@inheritdoc} */ public function getName() { return 'dialog'; } /** * Return a valid Unix shell. * * @return string|bool The valid shell name, false in case no valid shell is found */ private function getShell() { if (null !== self::$shell) { return self::$shell; } self::$shell = false; if (file_exists('/usr/bin/env')) { // handle other OSs with bash/zsh/ksh/csh if available to hide the answer $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) { if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { self::$shell = $sh; break; } } } return self::$shell; } private function hasSttyAvailable() { if (null !== self::$stty) { return self::$stty; } exec('stty 2>&1', $output, $exitcode); return self::$stty = $exitcode === 0; } /** * Validate an attempt. * * @param callable $interviewer A callable that will ask for a question and return the result * @param OutputInterface $output An Output instance * @param callable $validator A PHP callback * @param int|false $attempts Max number of times to ask before giving up; false will ask infinitely * * @return string The validated response * * @throws \Exception In case the max number of attempts has been reached and no valid response has been given */ private function validateAttempts($interviewer, OutputInterface $output, $validator, $attempts) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $e = null; while (false === $attempts || $attempts--) { if (null !== $e) { $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($e->getMessage(), 'error')); } try { return call_user_func($validator, $interviewer()); } catch (\Exception $e) { } } throw $e; } } * * 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\Formatter\OutputFormatter; /** * The Formatter class provides helpers to format messages. * * @author Fabien Potencier */ class FormatterHelper extends Helper { /** * Formats a message within a section. * * @param string $section The section name * @param string $message The message * @param string $style The style to apply to the section * * @return string The format section */ public function formatSection($section, $message, $style = 'info') { return sprintf('<%s>[%s] %s', $style, $section, $style, $message); } /** * Formats a message as a block of text. * * @param string|array $messages The message to write in the block * @param string $style The style to apply to the whole block * @param bool $large Whether to return a large block * * @return string The formatter message */ public function formatBlock($messages, $style, $large = false) { if (!is_array($messages)) { $messages = array($messages); } $len = 0; $lines = array(); foreach ($messages as $message) { $message = OutputFormatter::escape($message); $lines[] = sprintf($large ? ' %s ' : ' %s ', $message); $len = max($this->strlen($message) + ($large ? 4 : 2), $len); } $messages = $large ? array(str_repeat(' ', $len)) : array(); for ($i = 0; isset($lines[$i]); ++$i) { $messages[] = $lines[$i].str_repeat(' ', $len - $this->strlen($lines[$i])); } if ($large) { $messages[] = str_repeat(' ', $len); } for ($i = 0; isset($messages[$i]); ++$i) { $messages[$i] = sprintf('<%s>%s', $style, $messages[$i], $style); } return implode("\n", $messages); } /** * {@inheritdoc} */ public function getName() { return 'formatter'; } } * * 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\Formatter\OutputFormatterInterface; /** * Helper is the base class for all helper classes. * * @author Fabien Potencier */ abstract class Helper implements HelperInterface { protected $helperSet = null; /** * Sets the helper set associated with this helper. * * @param HelperSet $helperSet A HelperSet instance */ public function setHelperSet(HelperSet $helperSet = null) { $this->helperSet = $helperSet; } /** * Gets the helper set associated with this helper. * * @return HelperSet A HelperSet instance */ public function getHelperSet() { return $this->helperSet; } /** * Returns the length of a string, using mb_strwidth if it is available. * * @param string $string The string to check its length * * @return int The length of the string */ public static function strlen($string) { if (false === $encoding = mb_detect_encoding($string, null, true)) { return strlen($string); } return mb_strwidth($string, $encoding); } public static function formatTime($secs) { static $timeFormats = array( array(0, '< 1 sec'), array(1, '1 sec'), array(2, 'secs', 1), array(60, '1 min'), array(120, 'mins', 60), array(3600, '1 hr'), array(7200, 'hrs', 3600), array(86400, '1 day'), array(172800, 'days', 86400), ); foreach ($timeFormats as $index => $format) { if ($secs >= $format[0]) { if ((isset($timeFormats[$index + 1]) && $secs < $timeFormats[$index + 1][0]) || $index == count($timeFormats) - 1 ) { if (2 == count($format)) { return $format[1]; } return floor($secs / $format[2]).' '.$format[1]; } } } } public static function formatMemory($memory) { if ($memory >= 1024 * 1024 * 1024) { return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024); } if ($memory >= 1024 * 1024) { return sprintf('%.1f MiB', $memory / 1024 / 1024); } if ($memory >= 1024) { return sprintf('%d KiB', $memory / 1024); } return sprintf('%d B', $memory); } public static function strlenWithoutDecoration(OutputFormatterInterface $formatter, $string) { $isDecorated = $formatter->isDecorated(); $formatter->setDecorated(false); // remove <...> formatting $string = $formatter->format($string); // remove already formatted characters $string = preg_replace("/\033\[[^m]*m/", '', $string); $formatter->setDecorated($isDecorated); return self::strlen($string); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; /** * HelperInterface is the interface all helpers must implement. * * @author Fabien Potencier */ interface HelperInterface { /** * Sets the helper set associated with this helper. * * @param HelperSet $helperSet A HelperSet instance */ public function setHelperSet(HelperSet $helperSet = null); /** * Gets the helper set associated with this helper. * * @return HelperSet A HelperSet instance */ public function getHelperSet(); /** * Returns the canonical name of this helper. * * @return string The canonical name */ public function getName(); } * * 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\Command\Command; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * HelperSet represents a set of helpers to be used with a command. * * @author Fabien Potencier */ class HelperSet implements \IteratorAggregate { /** * @var Helper[] */ private $helpers = array(); private $command; /** * Constructor. * * @param Helper[] $helpers An array of helper */ public function __construct(array $helpers = array()) { foreach ($helpers as $alias => $helper) { $this->set($helper, is_int($alias) ? null : $alias); } } /** * Sets a helper. * * @param HelperInterface $helper The helper instance * @param string $alias An alias */ public function set(HelperInterface $helper, $alias = null) { $this->helpers[$helper->getName()] = $helper; if (null !== $alias) { $this->helpers[$alias] = $helper; } $helper->setHelperSet($this); } /** * Returns true if the helper if defined. * * @param string $name The helper name * * @return bool true if the helper is defined, false otherwise */ public function has($name) { return isset($this->helpers[$name]); } /** * Gets a helper value. * * @param string $name The helper name * * @return HelperInterface The helper instance * * @throws InvalidArgumentException if the helper is not defined */ public function get($name) { if (!$this->has($name)) { throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name)); } if ('dialog' === $name && $this->helpers[$name] instanceof DialogHelper) { @trigger_error('"Symfony\Component\Console\Helper\DialogHelper" is deprecated since version 2.5 and will be removed in 3.0. Use "Symfony\Component\Console\Helper\QuestionHelper" instead.', E_USER_DEPRECATED); } elseif ('progress' === $name && $this->helpers[$name] instanceof ProgressHelper) { @trigger_error('"Symfony\Component\Console\Helper\ProgressHelper" is deprecated since version 2.5 and will be removed in 3.0. Use "Symfony\Component\Console\Helper\ProgressBar" instead.', E_USER_DEPRECATED); } elseif ('table' === $name && $this->helpers[$name] instanceof TableHelper) { @trigger_error('"Symfony\Component\Console\Helper\TableHelper" is deprecated since version 2.5 and will be removed in 3.0. Use "Symfony\Component\Console\Helper\Table" instead.', E_USER_DEPRECATED); } return $this->helpers[$name]; } /** * Sets the command associated with this helper set. * * @param Command $command A Command instance */ public function setCommand(Command $command = null) { $this->command = $command; } /** * Gets the command associated with this helper set. * * @return Command A Command instance */ public function getCommand() { return $this->command; } /** * @return Helper[] */ public function getIterator() { return new \ArrayIterator($this->helpers); } } * * 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\Input\InputInterface; use Symfony\Component\Console\Input\InputAwareInterface; /** * An implementation of InputAwareInterface for Helpers. * * @author Wouter J */ abstract class InputAwareHelper extends Helper implements InputAwareInterface { protected $input; /** * {@inheritdoc} */ public function setInput(InputInterface $input) { $this->input = $input; } } * * 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\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; use Symfony\Component\Process\ProcessBuilder; /** * The ProcessHelper class provides helpers to run external processes. * * @author Fabien Potencier */ class ProcessHelper extends Helper { /** * Runs an external process. * * @param OutputInterface $output An OutputInterface instance * @param string|array|Process $cmd An instance of Process or an array of arguments to escape and run or a command to run * @param string|null $error An error message that must be displayed if something went wrong * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * @param int $verbosity The threshold for verbosity * * @return Process The process that ran */ public function run(OutputInterface $output, $cmd, $error = null, $callback = null, $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $formatter = $this->getHelperSet()->get('debug_formatter'); if (is_array($cmd)) { $process = ProcessBuilder::create($cmd)->getProcess(); } elseif ($cmd instanceof Process) { $process = $cmd; } else { $process = new Process($cmd); } if ($verbosity <= $output->getVerbosity()) { $output->write($formatter->start(spl_object_hash($process), $this->escapeString($process->getCommandLine()))); } if ($output->isDebug()) { $callback = $this->wrapCallback($output, $process, $callback); } $process->run($callback); if ($verbosity <= $output->getVerbosity()) { $message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run successfully', $process->getExitCode()); $output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful())); } if (!$process->isSuccessful() && null !== $error) { $output->writeln(sprintf('%s', $this->escapeString($error))); } return $process; } /** * Runs the process. * * This is identical to run() except that an exception is thrown if the process * exits with a non-zero exit code. * * @param OutputInterface $output An OutputInterface instance * @param string|Process $cmd An instance of Process or a command to run * @param string|null $error An error message that must be displayed if something went wrong * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return Process The process that ran * * @throws ProcessFailedException * * @see run() */ public function mustRun(OutputInterface $output, $cmd, $error = null, $callback = null) { $process = $this->run($output, $cmd, $error, $callback); if (!$process->isSuccessful()) { throw new ProcessFailedException($process); } return $process; } /** * Wraps a Process callback to add debugging output. * * @param OutputInterface $output An OutputInterface interface * @param Process $process The Process * @param callable|null $callback A PHP callable * * @return callable */ public function wrapCallback(OutputInterface $output, Process $process, $callback = null) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $formatter = $this->getHelperSet()->get('debug_formatter'); $that = $this; return function ($type, $buffer) use ($output, $process, $callback, $formatter, $that) { $output->write($formatter->progress(spl_object_hash($process), $that->escapeString($buffer), Process::ERR === $type)); if (null !== $callback) { call_user_func($callback, $type, $buffer); } }; } /** * This method is public for PHP 5.3 compatibility, it should be private. * * @internal */ public function escapeString($str) { return str_replace('<', '\\<', $str); } /** * {@inheritdoc} */ public function getName() { return 'process'; } } * * 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\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Exception\LogicException; /** * The ProgressBar provides helpers to display progress output. * * @author Fabien Potencier * @author Chris Jones */ class ProgressBar { // options private $barWidth = 28; private $barChar; private $emptyBarChar = '-'; private $progressChar = '>'; private $format; private $internalFormat; private $redrawFreq = 1; /** * @var OutputInterface */ private $output; private $step = 0; private $max; private $startTime; private $stepWidth; private $percent = 0.0; private $formatLineCount; private $messages = array(); private $overwrite = true; private $firstRun = true; private static $formatters; private static $formats; /** * Constructor. * * @param OutputInterface $output An OutputInterface instance * @param int $max Maximum steps (0 if unknown) */ public function __construct(OutputInterface $output, $max = 0) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $this->output = $output; $this->setMaxSteps($max); if (!$this->output->isDecorated()) { // disable overwrite when output does not support ANSI codes. $this->overwrite = false; // set a reasonable redraw frequency so output isn't flooded $this->setRedrawFrequency($max / 10); } $this->startTime = time(); } /** * Sets a placeholder formatter for a given name. * * This method also allow you to override an existing placeholder. * * @param string $name The placeholder name (including the delimiter char like %) * @param callable $callable A PHP callable */ public static function setPlaceholderFormatterDefinition($name, $callable) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } self::$formatters[$name] = $callable; } /** * Gets the placeholder formatter for a given name. * * @param string $name The placeholder name (including the delimiter char like %) * * @return callable|null A PHP callable */ public static function getPlaceholderFormatterDefinition($name) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } return isset(self::$formatters[$name]) ? self::$formatters[$name] : null; } /** * Sets a format for a given name. * * This method also allow you to override an existing format. * * @param string $name The format name * @param string $format A format string */ public static function setFormatDefinition($name, $format) { if (!self::$formats) { self::$formats = self::initFormats(); } self::$formats[$name] = $format; } /** * Gets the format for a given name. * * @param string $name The format name * * @return string|null A format string */ public static function getFormatDefinition($name) { if (!self::$formats) { self::$formats = self::initFormats(); } return isset(self::$formats[$name]) ? self::$formats[$name] : null; } /** * Associates a text with a named placeholder. * * The text is displayed when the progress bar is rendered but only * when the corresponding placeholder is part of the custom format line * (by wrapping the name with %). * * @param string $message The text to associate with the placeholder * @param string $name The name of the placeholder */ public function setMessage($message, $name = 'message') { $this->messages[$name] = $message; } public function getMessage($name = 'message') { return $this->messages[$name]; } /** * Gets the progress bar start time. * * @return int The progress bar start time */ public function getStartTime() { return $this->startTime; } /** * Gets the progress bar maximal steps. * * @return int The progress bar max steps */ public function getMaxSteps() { return $this->max; } /** * Gets the progress bar step. * * @deprecated since version 2.6, to be removed in 3.0. Use {@link getProgress()} instead. * * @return int The progress bar step */ public function getStep() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the getProgress() method instead.', E_USER_DEPRECATED); return $this->getProgress(); } /** * Gets the current step position. * * @return int The progress bar step */ public function getProgress() { return $this->step; } /** * Gets the progress bar step width. * * @internal This method is public for PHP 5.3 compatibility, it should not be used. * * @return int The progress bar step width */ public function getStepWidth() { return $this->stepWidth; } /** * Gets the current progress bar percent. * * @return float The current progress bar percent */ public function getProgressPercent() { return $this->percent; } /** * Sets the progress bar width. * * @param int $size The progress bar size */ public function setBarWidth($size) { $this->barWidth = (int) $size; } /** * Gets the progress bar width. * * @return int The progress bar size */ public function getBarWidth() { return $this->barWidth; } /** * Sets the bar character. * * @param string $char A character */ public function setBarCharacter($char) { $this->barChar = $char; } /** * Gets the bar character. * * @return string A character */ public function getBarCharacter() { if (null === $this->barChar) { return $this->max ? '=' : $this->emptyBarChar; } return $this->barChar; } /** * Sets the empty bar character. * * @param string $char A character */ public function setEmptyBarCharacter($char) { $this->emptyBarChar = $char; } /** * Gets the empty bar character. * * @return string A character */ public function getEmptyBarCharacter() { return $this->emptyBarChar; } /** * Sets the progress bar character. * * @param string $char A character */ public function setProgressCharacter($char) { $this->progressChar = $char; } /** * Gets the progress bar character. * * @return string A character */ public function getProgressCharacter() { return $this->progressChar; } /** * Sets the progress bar format. * * @param string $format The format */ public function setFormat($format) { $this->format = null; $this->internalFormat = $format; } /** * Sets the redraw frequency. * * @param int|float $freq The frequency in steps */ public function setRedrawFrequency($freq) { $this->redrawFreq = max((int) $freq, 1); } /** * Starts the progress output. * * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged */ public function start($max = null) { $this->startTime = time(); $this->step = 0; $this->percent = 0.0; if (null !== $max) { $this->setMaxSteps($max); } $this->display(); } /** * Advances the progress output X steps. * * @param int $step Number of steps to advance * * @throws LogicException */ public function advance($step = 1) { $this->setProgress($this->step + $step); } /** * Sets the current progress. * * @deprecated since version 2.6, to be removed in 3.0. Use {@link setProgress()} instead. * * @param int $step The current progress * * @throws LogicException */ public function setCurrent($step) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setProgress() method instead.', E_USER_DEPRECATED); $this->setProgress($step); } /** * Sets whether to overwrite the progressbar, false for new line. * * @param bool $overwrite */ public function setOverwrite($overwrite) { $this->overwrite = (bool) $overwrite; } /** * Sets the current progress. * * @param int $step The current progress * * @throws LogicException */ public function setProgress($step) { $step = (int) $step; if ($step < $this->step) { throw new LogicException('You can\'t regress the progress bar.'); } if ($this->max && $step > $this->max) { $this->max = $step; } $prevPeriod = (int) ($this->step / $this->redrawFreq); $currPeriod = (int) ($step / $this->redrawFreq); $this->step = $step; $this->percent = $this->max ? (float) $this->step / $this->max : 0; if ($prevPeriod !== $currPeriod || $this->max === $step) { $this->display(); } } /** * Finishes the progress output. */ public function finish() { if (!$this->max) { $this->max = $this->step; } if ($this->step === $this->max && !$this->overwrite) { // prevent double 100% output return; } $this->setProgress($this->max); } /** * Outputs the current progress string. */ public function display() { if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { return; } if (null === $this->format) { $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); } // these 3 variables can be removed in favor of using $this in the closure when support for PHP 5.3 will be dropped. $self = $this; $output = $this->output; $messages = $this->messages; $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self, $output, $messages) { if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) { $text = call_user_func($formatter, $self, $output); } elseif (isset($messages[$matches[1]])) { $text = $messages[$matches[1]]; } else { return $matches[0]; } if (isset($matches[2])) { $text = sprintf('%'.$matches[2], $text); } return $text; }, $this->format)); } /** * Removes the progress bar from the current line. * * This is useful if you wish to write some output * while a progress bar is running. * Call display() to show the progress bar again. */ public function clear() { if (!$this->overwrite) { return; } if (null === $this->format) { $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); } $this->overwrite(''); } /** * Sets the progress bar format. * * @param string $format The format */ private function setRealFormat($format) { // try to use the _nomax variant if available if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) { $this->format = self::getFormatDefinition($format.'_nomax'); } elseif (null !== self::getFormatDefinition($format)) { $this->format = self::getFormatDefinition($format); } else { $this->format = $format; } $this->formatLineCount = substr_count($this->format, "\n"); } /** * Sets the progress bar maximal steps. * * @param int $max The progress bar max steps */ private function setMaxSteps($max) { $this->max = max(0, (int) $max); $this->stepWidth = $this->max ? Helper::strlen($this->max) : 4; } /** * Overwrites a previous message to the output. * * @param string $message The message */ private function overwrite($message) { if ($this->overwrite) { if (!$this->firstRun) { // Move the cursor to the beginning of the line $this->output->write("\x0D"); // Erase the line $this->output->write("\x1B[2K"); // Erase previous lines if ($this->formatLineCount > 0) { $this->output->write(str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount)); } } } elseif ($this->step > 0) { $this->output->writeln(''); } $this->firstRun = false; $this->output->write($message); } private function determineBestFormat() { switch ($this->output->getVerbosity()) { // OutputInterface::VERBOSITY_QUIET: display is disabled anyway case OutputInterface::VERBOSITY_VERBOSE: return $this->max ? 'verbose' : 'verbose_nomax'; case OutputInterface::VERBOSITY_VERY_VERBOSE: return $this->max ? 'very_verbose' : 'very_verbose_nomax'; case OutputInterface::VERBOSITY_DEBUG: return $this->max ? 'debug' : 'debug_nomax'; default: return $this->max ? 'normal' : 'normal_nomax'; } } private static function initPlaceholderFormatters() { return array( 'bar' => function (ProgressBar $bar, OutputInterface $output) { $completeBars = floor($bar->getMaxSteps() > 0 ? $bar->getProgressPercent() * $bar->getBarWidth() : $bar->getProgress() % $bar->getBarWidth()); $display = str_repeat($bar->getBarCharacter(), $completeBars); if ($completeBars < $bar->getBarWidth()) { $emptyBars = $bar->getBarWidth() - $completeBars - Helper::strlenWithoutDecoration($output->getFormatter(), $bar->getProgressCharacter()); $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); } return $display; }, 'elapsed' => function (ProgressBar $bar) { return Helper::formatTime(time() - $bar->getStartTime()); }, 'remaining' => function (ProgressBar $bar) { if (!$bar->getMaxSteps()) { throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); } if (!$bar->getProgress()) { $remaining = 0; } else { $remaining = round((time() - $bar->getStartTime()) / $bar->getProgress() * ($bar->getMaxSteps() - $bar->getProgress())); } return Helper::formatTime($remaining); }, 'estimated' => function (ProgressBar $bar) { if (!$bar->getMaxSteps()) { throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); } if (!$bar->getProgress()) { $estimated = 0; } else { $estimated = round((time() - $bar->getStartTime()) / $bar->getProgress() * $bar->getMaxSteps()); } return Helper::formatTime($estimated); }, 'memory' => function (ProgressBar $bar) { return Helper::formatMemory(memory_get_usage(true)); }, 'current' => function (ProgressBar $bar) { return str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', STR_PAD_LEFT); }, 'max' => function (ProgressBar $bar) { return $bar->getMaxSteps(); }, 'percent' => function (ProgressBar $bar) { return floor($bar->getProgressPercent() * 100); }, ); } private static function initFormats() { return array( 'normal' => ' %current%/%max% [%bar%] %percent:3s%%', 'normal_nomax' => ' %current% [%bar%]', 'verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', 'verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', 'very_verbose' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', 'very_verbose_nomax' => ' %current% [%bar%] %elapsed:6s%', 'debug' => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', 'debug_nomax' => ' %current% [%bar%] %elapsed:6s% %memory:6s%', ); } } * * 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\NullOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Exception\LogicException; /** * The Progress class provides helpers to display progress output. * * @author Chris Jones * @author Fabien Potencier * * @deprecated since version 2.5, to be removed in 3.0 * Use {@link ProgressBar} instead. */ class ProgressHelper extends Helper { const FORMAT_QUIET = ' %percent%%'; const FORMAT_NORMAL = ' %current%/%max% [%bar%] %percent%%'; const FORMAT_VERBOSE = ' %current%/%max% [%bar%] %percent%% Elapsed: %elapsed%'; const FORMAT_QUIET_NOMAX = ' %current%'; const FORMAT_NORMAL_NOMAX = ' %current% [%bar%]'; const FORMAT_VERBOSE_NOMAX = ' %current% [%bar%] Elapsed: %elapsed%'; // options private $barWidth = 28; private $barChar = '='; private $emptyBarChar = '-'; private $progressChar = '>'; private $format = null; private $redrawFreq = 1; private $lastMessagesLength; private $barCharOriginal; /** * @var OutputInterface */ private $output; /** * Current step. * * @var int */ private $current; /** * Maximum number of steps. * * @var int */ private $max; /** * Start time of the progress bar. * * @var int */ private $startTime; /** * List of formatting variables. * * @var array */ private $defaultFormatVars = array( 'current', 'max', 'bar', 'percent', 'elapsed', ); /** * Available formatting variables. * * @var array */ private $formatVars; /** * Stored format part widths (used for padding). * * @var array */ private $widths = array( 'current' => 4, 'max' => 4, 'percent' => 3, 'elapsed' => 6, ); /** * Various time formats. * * @var array */ private $timeFormats = array( array(0, '???'), array(2, '1 sec'), array(59, 'secs', 1), array(60, '1 min'), array(3600, 'mins', 60), array(5400, '1 hr'), array(86400, 'hrs', 3600), array(129600, '1 day'), array(604800, 'days', 86400), ); public function __construct($triggerDeprecationError = true) { if ($triggerDeprecationError) { @trigger_error('The '.__CLASS__.' class is deprecated since version 2.5 and will be removed in 3.0. Use the Symfony\Component\Console\Helper\ProgressBar class instead.', E_USER_DEPRECATED); } } /** * Sets the progress bar width. * * @param int $size The progress bar size */ public function setBarWidth($size) { $this->barWidth = (int) $size; } /** * Sets the bar character. * * @param string $char A character */ public function setBarCharacter($char) { $this->barChar = $char; } /** * Sets the empty bar character. * * @param string $char A character */ public function setEmptyBarCharacter($char) { $this->emptyBarChar = $char; } /** * Sets the progress bar character. * * @param string $char A character */ public function setProgressCharacter($char) { $this->progressChar = $char; } /** * Sets the progress bar format. * * @param string $format The format */ public function setFormat($format) { $this->format = $format; } /** * Sets the redraw frequency. * * @param int $freq The frequency in steps */ public function setRedrawFrequency($freq) { $this->redrawFreq = (int) $freq; } /** * Starts the progress output. * * @param OutputInterface $output An Output instance * @param int|null $max Maximum steps */ public function start(OutputInterface $output, $max = null) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } $this->startTime = time(); $this->current = 0; $this->max = (int) $max; // Disabling output when it does not support ANSI codes as it would result in a broken display anyway. $this->output = $output->isDecorated() ? $output : new NullOutput(); $this->lastMessagesLength = 0; $this->barCharOriginal = ''; if (null === $this->format) { switch ($output->getVerbosity()) { case OutputInterface::VERBOSITY_QUIET: $this->format = self::FORMAT_QUIET_NOMAX; if ($this->max > 0) { $this->format = self::FORMAT_QUIET; } break; case OutputInterface::VERBOSITY_VERBOSE: case OutputInterface::VERBOSITY_VERY_VERBOSE: case OutputInterface::VERBOSITY_DEBUG: $this->format = self::FORMAT_VERBOSE_NOMAX; if ($this->max > 0) { $this->format = self::FORMAT_VERBOSE; } break; default: $this->format = self::FORMAT_NORMAL_NOMAX; if ($this->max > 0) { $this->format = self::FORMAT_NORMAL; } break; } } $this->initialize(); } /** * Advances the progress output X steps. * * @param int $step Number of steps to advance * @param bool $redraw Whether to redraw or not * * @throws LogicException */ public function advance($step = 1, $redraw = false) { $this->setCurrent($this->current + $step, $redraw); } /** * Sets the current progress. * * @param int $current The current progress * @param bool $redraw Whether to redraw or not * * @throws LogicException */ public function setCurrent($current, $redraw = false) { if (null === $this->startTime) { throw new LogicException('You must start the progress bar before calling setCurrent().'); } $current = (int) $current; if ($current < $this->current) { throw new LogicException('You can\'t regress the progress bar'); } if (0 === $this->current) { $redraw = true; } $prevPeriod = (int) ($this->current / $this->redrawFreq); $this->current = $current; $currPeriod = (int) ($this->current / $this->redrawFreq); if ($redraw || $prevPeriod !== $currPeriod || $this->max === $this->current) { $this->display(); } } /** * Outputs the current progress string. * * @param bool $finish Forces the end result * * @throws LogicException */ public function display($finish = false) { if (null === $this->startTime) { throw new LogicException('You must start the progress bar before calling display().'); } $message = $this->format; foreach ($this->generate($finish) as $name => $value) { $message = str_replace("%{$name}%", $value, $message); } $this->overwrite($this->output, $message); } /** * Removes the progress bar from the current line. * * This is useful if you wish to write some output * while a progress bar is running. * Call display() to show the progress bar again. */ public function clear() { $this->overwrite($this->output, ''); } /** * Finishes the progress output. */ public function finish() { if (null === $this->startTime) { throw new LogicException('You must start the progress bar before calling finish().'); } if (null !== $this->startTime) { if (!$this->max) { $this->barChar = $this->barCharOriginal; $this->display(true); } $this->startTime = null; $this->output->writeln(''); $this->output = null; } } /** * Initializes the progress helper. */ private function initialize() { $this->formatVars = array(); foreach ($this->defaultFormatVars as $var) { if (false !== strpos($this->format, "%{$var}%")) { $this->formatVars[$var] = true; } } if ($this->max > 0) { $this->widths['max'] = $this->strlen($this->max); $this->widths['current'] = $this->widths['max']; } else { $this->barCharOriginal = $this->barChar; $this->barChar = $this->emptyBarChar; } } /** * Generates the array map of format variables to values. * * @param bool $finish Forces the end result * * @return array Array of format vars and values */ private function generate($finish = false) { $vars = array(); $percent = 0; if ($this->max > 0) { $percent = (float) $this->current / $this->max; } if (isset($this->formatVars['bar'])) { if ($this->max > 0) { $completeBars = floor($percent * $this->barWidth); } else { if (!$finish) { $completeBars = floor($this->current % $this->barWidth); } else { $completeBars = $this->barWidth; } } $emptyBars = $this->barWidth - $completeBars - $this->strlen($this->progressChar); $bar = str_repeat($this->barChar, $completeBars); if ($completeBars < $this->barWidth) { $bar .= $this->progressChar; $bar .= str_repeat($this->emptyBarChar, $emptyBars); } $vars['bar'] = $bar; } if (isset($this->formatVars['elapsed'])) { $elapsed = time() - $this->startTime; $vars['elapsed'] = str_pad($this->humaneTime($elapsed), $this->widths['elapsed'], ' ', STR_PAD_LEFT); } if (isset($this->formatVars['current'])) { $vars['current'] = str_pad($this->current, $this->widths['current'], ' ', STR_PAD_LEFT); } if (isset($this->formatVars['max'])) { $vars['max'] = $this->max; } if (isset($this->formatVars['percent'])) { $vars['percent'] = str_pad(floor($percent * 100), $this->widths['percent'], ' ', STR_PAD_LEFT); } return $vars; } /** * Converts seconds into human-readable format. * * @param int $secs Number of seconds * * @return string Time in readable format */ private function humaneTime($secs) { $text = ''; foreach ($this->timeFormats as $format) { if ($secs < $format[0]) { if (count($format) == 2) { $text = $format[1]; break; } else { $text = ceil($secs / $format[2]).' '.$format[1]; break; } } } return $text; } /** * Overwrites a previous message to the output. * * @param OutputInterface $output An Output instance * @param string $message The message */ private function overwrite(OutputInterface $output, $message) { $length = $this->strlen($message); // append whitespace to match the last line's length if (null !== $this->lastMessagesLength && $this->lastMessagesLength > $length) { $message = str_pad($message, $this->lastMessagesLength, "\x20", STR_PAD_RIGHT); } // carriage return $output->write("\x0D"); $output->write($message); $this->lastMessagesLength = $this->strlen($message); } /** * {@inheritdoc} */ public function getName() { return 'progress'; } } * * 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\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Output\OutputInterface; /** * @author Kevin Bond */ class ProgressIndicator { private $output; private $startTime; private $format; private $message; private $indicatorValues; private $indicatorCurrent; private $indicatorChangeInterval; private $indicatorUpdateTime; private $started = false; private static $formatters; private static $formats; /** * @param OutputInterface $output * @param string|null $format Indicator format * @param int $indicatorChangeInterval Change interval in milliseconds * @param array|null $indicatorValues Animated indicator characters */ public function __construct(OutputInterface $output, $format = null, $indicatorChangeInterval = 100, $indicatorValues = null) { $this->output = $output; if (null === $format) { $format = $this->determineBestFormat(); } if (null === $indicatorValues) { $indicatorValues = array('-', '\\', '|', '/'); } $indicatorValues = array_values($indicatorValues); if (2 > count($indicatorValues)) { throw new InvalidArgumentException('Must have at least 2 indicator value characters.'); } $this->format = self::getFormatDefinition($format); $this->indicatorChangeInterval = $indicatorChangeInterval; $this->indicatorValues = $indicatorValues; $this->startTime = time(); } /** * Sets the current indicator message. * * @param string|null $message */ public function setMessage($message) { $this->message = $message; $this->display(); } /** * Gets the current indicator message. * * @return string|null * * @internal for PHP 5.3 compatibility */ public function getMessage() { return $this->message; } /** * Gets the progress bar start time. * * @return int The progress bar start time * * @internal for PHP 5.3 compatibility */ public function getStartTime() { return $this->startTime; } /** * Gets the current animated indicator character. * * @return string * * @internal for PHP 5.3 compatibility */ public function getCurrentValue() { return $this->indicatorValues[$this->indicatorCurrent % count($this->indicatorValues)]; } /** * Starts the indicator output. * * @param $message */ public function start($message) { if ($this->started) { throw new LogicException('Progress indicator already started.'); } $this->message = $message; $this->started = true; $this->startTime = time(); $this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval; $this->indicatorCurrent = 0; $this->display(); } /** * Advances the indicator. */ public function advance() { if (!$this->started) { throw new LogicException('Progress indicator has not yet been started.'); } if (!$this->output->isDecorated()) { return; } $currentTime = $this->getCurrentTimeInMilliseconds(); if ($currentTime < $this->indicatorUpdateTime) { return; } $this->indicatorUpdateTime = $currentTime + $this->indicatorChangeInterval; ++$this->indicatorCurrent; $this->display(); } /** * Finish the indicator with message. * * @param $message */ public function finish($message) { if (!$this->started) { throw new LogicException('Progress indicator has not yet been started.'); } $this->message = $message; $this->display(); $this->output->writeln(''); $this->started = false; } /** * Gets the format for a given name. * * @param string $name The format name * * @return string|null A format string */ public static function getFormatDefinition($name) { if (!self::$formats) { self::$formats = self::initFormats(); } return isset(self::$formats[$name]) ? self::$formats[$name] : null; } /** * Sets a placeholder formatter for a given name. * * This method also allow you to override an existing placeholder. * * @param string $name The placeholder name (including the delimiter char like %) * @param callable $callable A PHP callable */ public static function setPlaceholderFormatterDefinition($name, $callable) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } self::$formatters[$name] = $callable; } /** * Gets the placeholder formatter for a given name. * * @param string $name The placeholder name (including the delimiter char like %) * * @return callable|null A PHP callable */ public static function getPlaceholderFormatterDefinition($name) { if (!self::$formatters) { self::$formatters = self::initPlaceholderFormatters(); } return isset(self::$formatters[$name]) ? self::$formatters[$name] : null; } private function display() { if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { return; } $self = $this; $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) use ($self) { if ($formatter = $self::getPlaceholderFormatterDefinition($matches[1])) { return call_user_func($formatter, $self); } return $matches[0]; }, $this->format)); } private function determineBestFormat() { switch ($this->output->getVerbosity()) { // OutputInterface::VERBOSITY_QUIET: display is disabled anyway case OutputInterface::VERBOSITY_VERBOSE: return $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi'; case OutputInterface::VERBOSITY_VERY_VERBOSE: case OutputInterface::VERBOSITY_DEBUG: return $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi'; default: return $this->output->isDecorated() ? 'normal' : 'normal_no_ansi'; } } /** * Overwrites a previous message to the output. * * @param string $message The message */ private function overwrite($message) { if ($this->output->isDecorated()) { $this->output->write("\x0D\x1B[2K"); $this->output->write($message); } else { $this->output->writeln($message); } } private function getCurrentTimeInMilliseconds() { return round(microtime(true) * 1000); } private static function initPlaceholderFormatters() { return array( 'indicator' => function (ProgressIndicator $indicator) { return $indicator->getCurrentValue(); }, 'message' => function (ProgressIndicator $indicator) { return $indicator->getMessage(); }, 'elapsed' => function (ProgressIndicator $indicator) { return Helper::formatTime(time() - $indicator->getStartTime()); }, 'memory' => function () { return Helper::formatMemory(memory_get_usage(true)); }, ); } private static function initFormats() { return array( 'normal' => ' %indicator% %message%', 'normal_no_ansi' => ' %message%', 'verbose' => ' %indicator% %message% (%elapsed:6s%)', 'verbose_no_ansi' => ' %message% (%elapsed:6s%)', 'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)', 'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)', ); } } * * 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\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Question\ChoiceQuestion; /** * The QuestionHelper class provides helpers to interact with the user. * * @author Fabien Potencier */ class QuestionHelper extends Helper { private $inputStream; private static $shell; private static $stty; /** * Asks a question to the user. * * @param InputInterface $input An InputInterface instance * @param OutputInterface $output An OutputInterface instance * @param Question $question The question to ask * * @return string The user answer * * @throws RuntimeException If there is no data to read in the input stream */ public function ask(InputInterface $input, OutputInterface $output, Question $question) { if ($output instanceof ConsoleOutputInterface) { $output = $output->getErrorOutput(); } if (!$input->isInteractive()) { return $question->getDefault(); } if (!$question->getValidator()) { return $this->doAsk($output, $question); } $that = $this; $interviewer = function () use ($output, $question, $that) { return $that->doAsk($output, $question); }; return $this->validateAttempts($interviewer, $output, $question); } /** * Sets the input stream to read from when interacting with the user. * * This is mainly useful for testing purpose. * * @param resource $stream The input stream * * @throws InvalidArgumentException In case the stream is not a resource */ public function setInputStream($stream) { if (!is_resource($stream)) { throw new InvalidArgumentException('Input stream must be a valid resource.'); } $this->inputStream = $stream; } /** * Returns the helper's input stream. * * @return resource */ public function getInputStream() { return $this->inputStream; } /** * {@inheritdoc} */ public function getName() { return 'question'; } /** * Asks the question to the user. * * This method is public for PHP 5.3 compatibility, it should be private. * * @param OutputInterface $output * @param Question $question * * @return bool|mixed|null|string * * @throws \Exception * @throws \RuntimeException */ public function doAsk(OutputInterface $output, Question $question) { $this->writePrompt($output, $question); $inputStream = $this->inputStream ?: STDIN; $autocomplete = $question->getAutocompleterValues(); if (null === $autocomplete || !$this->hasSttyAvailable()) { $ret = false; if ($question->isHidden()) { try { $ret = trim($this->getHiddenResponse($output, $inputStream)); } catch (\RuntimeException $e) { if (!$question->isHiddenFallback()) { throw $e; } } } if (false === $ret) { $ret = fgets($inputStream, 4096); if (false === $ret) { throw new RuntimeException('Aborted'); } $ret = trim($ret); } } else { $ret = trim($this->autocomplete($output, $question, $inputStream)); } $ret = strlen($ret) > 0 ? $ret : $question->getDefault(); if ($normalizer = $question->getNormalizer()) { return $normalizer($ret); } return $ret; } /** * Outputs the question prompt. * * @param OutputInterface $output * @param Question $question */ protected function writePrompt(OutputInterface $output, Question $question) { $message = $question->getQuestion(); if ($question instanceof ChoiceQuestion) { $maxWidth = max(array_map(array($this, 'strlen'), array_keys($question->getChoices()))); $messages = (array) $question->getQuestion(); foreach ($question->getChoices() as $key => $value) { $width = $maxWidth - $this->strlen($key); $messages[] = ' ['.$key.str_repeat(' ', $width).'] '.$value; } $output->writeln($messages); $message = $question->getPrompt(); } $output->write($message); } /** * Outputs an error message. * * @param OutputInterface $output * @param \Exception $error */ protected function writeError(OutputInterface $output, \Exception $error) { if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) { $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'); } else { $message = ''.$error->getMessage().''; } $output->writeln($message); } /** * Autocompletes a question. * * @param OutputInterface $output * @param Question $question * @param resource $inputStream * * @return string */ private function autocomplete(OutputInterface $output, Question $question, $inputStream) { $autocomplete = $question->getAutocompleterValues(); $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 (!feof($inputStream)) { $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 (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { if ('A' === $c[2] && -1 === $ofs) { $ofs = 0; } if (0 === $numMatches) { continue; } $ofs += ('A' === $c[2]) ? -1 : 1; $ofs = ($numMatches + $ofs) % $numMatches; } } elseif (ord($c) < 32) { if ("\t" === $c || "\n" === $c) { if ($numMatches > 0 && -1 !== $ofs) { $ret = $matches[$ofs]; // Echo out remaining chars for current match $output->write(substr($ret, $i)); $i = strlen($ret); } if ("\n" === $c) { $output->write($c); break; } $numMatches = 0; } continue; } else { $output->write($c); $ret .= $c; ++$i; $numMatches = 0; $ofs = 0; foreach ($autocomplete as $value) { // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) if (0 === strpos($value, $ret) && $i !== strlen($value)) { $matches[$numMatches++] = $value; } } } // Erase characters from cursor to end of line $output->write("\033[K"); if ($numMatches > 0 && -1 !== $ofs) { // Save cursor position $output->write("\0337"); // Write highlighted text $output->write(''.substr($matches[$ofs], $i).''); // Restore cursor position $output->write("\0338"); } } // Reset stty so it behaves normally again shell_exec(sprintf('stty %s', $sttyMode)); return $ret; } /** * Gets a hidden response from user. * * @param OutputInterface $output An Output instance * @param resource $inputStream The handler resource * * @return string The answer * * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden */ private function getHiddenResponse(OutputInterface $output, $inputStream) { if ('\\' === DIRECTORY_SEPARATOR) { $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; // handle code running from a phar if ('phar:' === substr(__FILE__, 0, 5)) { $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; copy($exe, $tmpExe); $exe = $tmpExe; } $value = rtrim(shell_exec($exe)); $output->writeln(''); if (isset($tmpExe)) { unlink($tmpExe); } return $value; } if ($this->hasSttyAvailable()) { $sttyMode = shell_exec('stty -g'); shell_exec('stty -echo'); $value = fgets($inputStream, 4096); shell_exec(sprintf('stty %s', $sttyMode)); if (false === $value) { throw new RuntimeException('Aborted'); } $value = trim($value); $output->writeln(''); return $value; } if (false !== $shell = $this->getShell()) { $readCmd = $shell === 'csh' ? 'set mypassword = $<' : 'read -r mypassword'; $command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd); $value = rtrim(shell_exec($command)); $output->writeln(''); return $value; } throw new RuntimeException('Unable to hide the response.'); } /** * Validates an attempt. * * @param callable $interviewer A callable that will ask for a question and return the result * @param OutputInterface $output An Output instance * @param Question $question A Question instance * * @return string The validated response * * @throws \Exception In case the max number of attempts has been reached and no valid response has been given */ private function validateAttempts($interviewer, OutputInterface $output, Question $question) { $error = null; $attempts = $question->getMaxAttempts(); while (null === $attempts || $attempts--) { if (null !== $error) { $this->writeError($output, $error); } try { return call_user_func($question->getValidator(), $interviewer()); } catch (RuntimeException $e) { throw $e; } catch (\Exception $error) { } } throw $error; } /** * Returns a valid unix shell. * * @return string|bool The valid shell name, false in case no valid shell is found */ private function getShell() { if (null !== self::$shell) { return self::$shell; } self::$shell = false; if (file_exists('/usr/bin/env')) { // handle other OSs with bash/zsh/ksh/csh if available to hide the answer $test = "/usr/bin/env %s -c 'echo OK' 2> /dev/null"; foreach (array('bash', 'zsh', 'ksh', 'csh') as $sh) { if ('OK' === rtrim(shell_exec(sprintf($test, $sh)))) { self::$shell = $sh; break; } } } return self::$shell; } /** * Returns whether Stty is available or not. * * @return bool */ private function hasSttyAvailable() { if (null !== self::$stty) { return self::$stty; } exec('stty 2>&1', $output, $exitcode); return self::$stty = $exitcode === 0; } } * * 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\Exception\LogicException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Formatter\OutputFormatter; /** * Symfony Style Guide compliant question helper. * * @author Kevin Bond */ class SymfonyQuestionHelper extends QuestionHelper { /** * {@inheritdoc} */ public function ask(InputInterface $input, OutputInterface $output, Question $question) { $validator = $question->getValidator(); $question->setValidator(function ($value) use ($validator) { if (null !== $validator) { $value = $validator($value); } else { // make required if (!is_array($value) && !is_bool($value) && 0 === strlen($value)) { throw new LogicException('A value is required.'); } } return $value; }); return parent::ask($input, $output, $question); } /** * {@inheritdoc} */ protected function writePrompt(OutputInterface $output, Question $question) { $text = OutputFormatter::escape($question->getQuestion()); $default = $question->getDefault(); switch (true) { case null === $default: $text = sprintf(' %s:', $text); break; case $question instanceof ConfirmationQuestion: $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); break; case $question instanceof ChoiceQuestion && $question->isMultiselect(): $choices = $question->getChoices(); $default = explode(',', $default); foreach ($default as $key => $value) { $default[$key] = $choices[trim($value)]; } $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape(implode(', ', $default))); break; case $question instanceof ChoiceQuestion: $choices = $question->getChoices(); $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($choices[$default])); break; default: $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($default)); } $output->writeln($text); if ($question instanceof ChoiceQuestion) { $width = max(array_map('strlen', array_keys($question->getChoices()))); foreach ($question->getChoices() as $key => $value) { $output->writeln(sprintf(" [%-${width}s] %s", $key, $value)); } } $output->write(' > '); } /** * {@inheritdoc} */ protected function writeError(OutputInterface $output, \Exception $error) { if ($output instanceof SymfonyStyle) { $output->newLine(); $output->error($error->getMessage()); return; } parent::writeError($output, $error); } } * * 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\Exception\InvalidArgumentException; /** * Provides helpers to display a table. * * @author Fabien Potencier * @author Саша Стаменковић * @author Abdellatif Ait boudad * @author Max Grigorian */ class Table { /** * Table headers. * * @var array */ private $headers = array(); /** * Table rows. * * @var array */ private $rows = array(); /** * Column widths cache. * * @var array */ private $columnWidths = array(); /** * Number of columns cache. * * @var array */ private $numberOfColumns; /** * @var OutputInterface */ private $output; /** * @var TableStyle */ private $style; /** * @var array */ private $columnStyles = array(); private static $styles; public function __construct(OutputInterface $output) { $this->output = $output; if (!self::$styles) { self::$styles = self::initStyles(); } $this->setStyle('default'); } /** * Sets a style definition. * * @param string $name The style name * @param TableStyle $style A TableStyle instance */ public static function setStyleDefinition($name, TableStyle $style) { if (!self::$styles) { self::$styles = self::initStyles(); } self::$styles[$name] = $style; } /** * Gets a style definition by name. * * @param string $name The style name * * @return TableStyle A TableStyle instance */ public static function getStyleDefinition($name) { if (!self::$styles) { self::$styles = self::initStyles(); } if (isset(self::$styles[$name])) { return self::$styles[$name]; } throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); } /** * Sets table style. * * @param TableStyle|string $name The style name or a TableStyle instance * * @return Table */ public function setStyle($name) { $this->style = $this->resolveStyle($name); return $this; } /** * Gets the current table style. * * @return TableStyle */ public function getStyle() { return $this->style; } /** * Sets table column style. * * @param int $columnIndex Column index * @param TableStyle|string $name The style name or a TableStyle instance * * @return Table */ public function setColumnStyle($columnIndex, $name) { $columnIndex = intval($columnIndex); $this->columnStyles[$columnIndex] = $this->resolveStyle($name); return $this; } /** * Gets the current style for a column. * * If style was not set, it returns the global table style. * * @param int $columnIndex Column index * * @return TableStyle */ public function getColumnStyle($columnIndex) { if (isset($this->columnStyles[$columnIndex])) { return $this->columnStyles[$columnIndex]; } return $this->getStyle(); } public function setHeaders(array $headers) { $headers = array_values($headers); if (!empty($headers) && !is_array($headers[0])) { $headers = array($headers); } $this->headers = $headers; return $this; } public function setRows(array $rows) { $this->rows = array(); return $this->addRows($rows); } public function addRows(array $rows) { foreach ($rows as $row) { $this->addRow($row); } return $this; } public function addRow($row) { if ($row instanceof TableSeparator) { $this->rows[] = $row; return $this; } if (!is_array($row)) { throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.'); } $this->rows[] = array_values($row); return $this; } public function setRow($column, array $row) { $this->rows[$column] = $row; return $this; } /** * Renders table to output. * * Example: * +---------------+-----------------------+------------------+ * | ISBN | Title | Author | * +---------------+-----------------------+------------------+ * | 99921-58-10-7 | Divine Comedy | Dante Alighieri | * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | * +---------------+-----------------------+------------------+ */ public function render() { $this->calculateNumberOfColumns(); $rows = $this->buildTableRows($this->rows); $headers = $this->buildTableRows($this->headers); $this->calculateColumnsWidth(array_merge($headers, $rows)); $this->renderRowSeparator(); if (!empty($headers)) { foreach ($headers as $header) { $this->renderRow($header, $this->style->getCellHeaderFormat()); $this->renderRowSeparator(); } } foreach ($rows as $row) { if ($row instanceof TableSeparator) { $this->renderRowSeparator(); } else { $this->renderRow($row, $this->style->getCellRowFormat()); } } if (!empty($rows)) { $this->renderRowSeparator(); } $this->cleanup(); } /** * Renders horizontal header separator. * * Example: +-----+-----------+-------+ */ private function renderRowSeparator() { if (0 === $count = $this->numberOfColumns) { return; } if (!$this->style->getHorizontalBorderChar() && !$this->style->getCrossingChar()) { return; } $markup = $this->style->getCrossingChar(); for ($column = 0; $column < $count; ++$column) { $markup .= str_repeat($this->style->getHorizontalBorderChar(), $this->columnWidths[$column]).$this->style->getCrossingChar(); } $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup)); } /** * Renders vertical column separator. */ private function renderColumnSeparator() { return sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar()); } /** * Renders table row. * * Example: | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | * * @param array $row * @param string $cellFormat */ private function renderRow(array $row, $cellFormat) { if (empty($row)) { return; } $rowContent = $this->renderColumnSeparator(); foreach ($this->getRowColumns($row) as $column) { $rowContent .= $this->renderCell($row, $column, $cellFormat); $rowContent .= $this->renderColumnSeparator(); } $this->output->writeln($rowContent); } /** * Renders table cell with padding. * * @param array $row * @param int $column * @param string $cellFormat */ private function renderCell(array $row, $column, $cellFormat) { $cell = isset($row[$column]) ? $row[$column] : ''; $width = $this->columnWidths[$column]; if ($cell instanceof TableCell && $cell->getColspan() > 1) { // add the width of the following columns(numbers of colspan). foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) { $width += $this->getColumnSeparatorWidth() + $this->columnWidths[$nextColumn]; } } // str_pad won't work properly with multi-byte strings, we need to fix the padding if (false !== $encoding = mb_detect_encoding($cell, null, true)) { $width += strlen($cell) - mb_strwidth($cell, $encoding); } $style = $this->getColumnStyle($column); if ($cell instanceof TableSeparator) { return sprintf($style->getBorderFormat(), str_repeat($style->getHorizontalBorderChar(), $width)); } $width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); $content = sprintf($style->getCellRowContentFormat(), $cell); return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $style->getPadType())); } /** * Calculate number of columns for this table. */ private function calculateNumberOfColumns() { if (null !== $this->numberOfColumns) { return; } $columns = array(0); foreach (array_merge($this->headers, $this->rows) as $row) { if ($row instanceof TableSeparator) { continue; } $columns[] = $this->getNumberOfColumns($row); } $this->numberOfColumns = max($columns); } private function buildTableRows($rows) { $unmergedRows = array(); for ($rowKey = 0; $rowKey < count($rows); ++$rowKey) { $rows = $this->fillNextRows($rows, $rowKey); // Remove any new line breaks and replace it with a new line foreach ($rows[$rowKey] as $column => $cell) { if (!strstr($cell, "\n")) { continue; } $lines = explode("\n", $cell); foreach ($lines as $lineKey => $line) { if ($cell instanceof TableCell) { $line = new TableCell($line, array('colspan' => $cell->getColspan())); } if (0 === $lineKey) { $rows[$rowKey][$column] = $line; } else { $unmergedRows[$rowKey][$lineKey][$column] = $line; } } } } $tableRows = array(); foreach ($rows as $rowKey => $row) { $tableRows[] = $this->fillCells($row); if (isset($unmergedRows[$rowKey])) { $tableRows = array_merge($tableRows, $unmergedRows[$rowKey]); } } return $tableRows; } /** * fill rows that contains rowspan > 1. * * @param array $rows * @param int $line * * @return array */ private function fillNextRows($rows, $line) { $unmergedRows = array(); foreach ($rows[$line] as $column => $cell) { if ($cell instanceof TableCell && $cell->getRowspan() > 1) { $nbLines = $cell->getRowspan() - 1; $lines = array($cell); if (strstr($cell, "\n")) { $lines = explode("\n", $cell); $nbLines = count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines; $rows[$line][$column] = new TableCell($lines[0], array('colspan' => $cell->getColspan())); unset($lines[0]); } // create a two dimensional array (rowspan x colspan) $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, array()), $unmergedRows); foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { $value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : ''; $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, array('colspan' => $cell->getColspan())); } } } foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { // we need to know if $unmergedRow will be merged or inserted into $rows if (isset($rows[$unmergedRowKey]) && is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) { foreach ($unmergedRow as $cellKey => $cell) { // insert cell into row at cellKey position array_splice($rows[$unmergedRowKey], $cellKey, 0, array($cell)); } } else { $row = $this->copyRow($rows, $unmergedRowKey - 1); foreach ($unmergedRow as $column => $cell) { if (!empty($cell)) { $row[$column] = $unmergedRow[$column]; } } array_splice($rows, $unmergedRowKey, 0, array($row)); } } return $rows; } /** * fill cells for a row that contains colspan > 1. * * @param array $row * * @return array */ private function fillCells($row) { $newRow = array(); foreach ($row as $column => $cell) { $newRow[] = $cell; if ($cell instanceof TableCell && $cell->getColspan() > 1) { foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) { // insert empty value at column position $newRow[] = ''; } } } return $newRow ?: $row; } /** * @param array $rows * @param int $line * * @return array */ private function copyRow($rows, $line) { $row = $rows[$line]; foreach ($row as $cellKey => $cellValue) { $row[$cellKey] = ''; if ($cellValue instanceof TableCell) { $row[$cellKey] = new TableCell('', array('colspan' => $cellValue->getColspan())); } } return $row; } /** * Gets number of columns by row. * * @param array $row * * @return int */ private function getNumberOfColumns(array $row) { $columns = count($row); foreach ($row as $column) { $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0; } return $columns; } /** * Gets list of columns for the given row. * * @param array $row * * @return array */ private function getRowColumns($row) { $columns = range(0, $this->numberOfColumns - 1); foreach ($row as $cellKey => $cell) { if ($cell instanceof TableCell && $cell->getColspan() > 1) { // exclude grouped columns. $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1)); } } return $columns; } /** * Calculates columns widths. * * @param array $rows */ private function calculateColumnsWidth($rows) { for ($column = 0; $column < $this->numberOfColumns; ++$column) { $lengths = array(); foreach ($rows as $row) { if ($row instanceof TableSeparator) { continue; } foreach ($row as $i => $cell) { if ($cell instanceof TableCell) { $textLength = strlen($cell); if ($textLength > 0) { $contentColumns = str_split($cell, ceil($textLength / $cell->getColspan())); foreach ($contentColumns as $position => $content) { $row[$i + $position] = $content; } } } } $lengths[] = $this->getCellWidth($row, $column); } $this->columnWidths[$column] = max($lengths) + strlen($this->style->getCellRowContentFormat()) - 2; } } /** * Gets column width. * * @return int */ private function getColumnSeparatorWidth() { return strlen(sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar())); } /** * Gets cell width. * * @param array $row * @param int $column * * @return int */ private function getCellWidth(array $row, $column) { if (isset($row[$column])) { $cell = $row[$column]; $cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); return $cellWidth; } return 0; } /** * Called after rendering to cleanup cache data. */ private function cleanup() { $this->columnWidths = array(); $this->numberOfColumns = null; } private static function initStyles() { $borderless = new TableStyle(); $borderless ->setHorizontalBorderChar('=') ->setVerticalBorderChar(' ') ->setCrossingChar(' ') ; $compact = new TableStyle(); $compact ->setHorizontalBorderChar('') ->setVerticalBorderChar(' ') ->setCrossingChar('') ->setCellRowContentFormat('%s') ; $styleGuide = new TableStyle(); $styleGuide ->setHorizontalBorderChar('-') ->setVerticalBorderChar(' ') ->setCrossingChar(' ') ->setCellHeaderFormat('%s') ; return array( 'default' => new TableStyle(), 'borderless' => $borderless, 'compact' => $compact, 'symfony-style-guide' => $styleGuide, ); } private function resolveStyle($name) { if ($name instanceof TableStyle) { return $name; } if (isset(self::$styles[$name])) { return self::$styles[$name]; } throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); } } * * 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\Exception\InvalidArgumentException; /** * @author Abdellatif Ait boudad */ class TableCell { /** * @var string */ private $value; /** * @var array */ private $options = array( 'rowspan' => 1, 'colspan' => 1, ); /** * @param string $value * @param array $options */ public function __construct($value = '', array $options = array()) { $this->value = $value; // check option names if ($diff = array_diff(array_keys($options), array_keys($this->options))) { throw new InvalidArgumentException(sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff))); } $this->options = array_merge($this->options, $options); } /** * Returns the cell value. * * @return string */ public function __toString() { return $this->value; } /** * Gets number of colspan. * * @return int */ public function getColspan() { return (int) $this->options['colspan']; } /** * Gets number of rowspan. * * @return int */ public function getRowspan() { return (int) $this->options['rowspan']; } } * * 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\Output\NullOutput; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * Provides helpers to display table output. * * @author Саша Стаменковић * @author Fabien Potencier * * @deprecated since version 2.5, to be removed in 3.0 * Use {@link Table} instead. */ class TableHelper extends Helper { const LAYOUT_DEFAULT = 0; const LAYOUT_BORDERLESS = 1; const LAYOUT_COMPACT = 2; /** * @var Table */ private $table; public function __construct($triggerDeprecationError = true) { if ($triggerDeprecationError) { @trigger_error('The '.__CLASS__.' class is deprecated since version 2.5 and will be removed in 3.0. Use the Symfony\Component\Console\Helper\Table class instead.', E_USER_DEPRECATED); } $this->table = new Table(new NullOutput()); } /** * Sets table layout type. * * @param int $layout self::LAYOUT_* * * @return TableHelper * * @throws InvalidArgumentException when the table layout is not known */ public function setLayout($layout) { switch ($layout) { case self::LAYOUT_BORDERLESS: $this->table->setStyle('borderless'); break; case self::LAYOUT_COMPACT: $this->table->setStyle('compact'); break; case self::LAYOUT_DEFAULT: $this->table->setStyle('default'); break; default: throw new InvalidArgumentException(sprintf('Invalid table layout "%s".', $layout)); } return $this; } public function setHeaders(array $headers) { $this->table->setHeaders($headers); return $this; } public function setRows(array $rows) { $this->table->setRows($rows); return $this; } public function addRows(array $rows) { $this->table->addRows($rows); return $this; } public function addRow(array $row) { $this->table->addRow($row); return $this; } public function setRow($column, array $row) { $this->table->setRow($column, $row); return $this; } /** * Sets padding character, used for cell padding. * * @param string $paddingChar * * @return TableHelper */ public function setPaddingChar($paddingChar) { $this->table->getStyle()->setPaddingChar($paddingChar); return $this; } /** * Sets horizontal border character. * * @param string $horizontalBorderChar * * @return TableHelper */ public function setHorizontalBorderChar($horizontalBorderChar) { $this->table->getStyle()->setHorizontalBorderChar($horizontalBorderChar); return $this; } /** * Sets vertical border character. * * @param string $verticalBorderChar * * @return TableHelper */ public function setVerticalBorderChar($verticalBorderChar) { $this->table->getStyle()->setVerticalBorderChar($verticalBorderChar); return $this; } /** * Sets crossing character. * * @param string $crossingChar * * @return TableHelper */ public function setCrossingChar($crossingChar) { $this->table->getStyle()->setCrossingChar($crossingChar); return $this; } /** * Sets header cell format. * * @param string $cellHeaderFormat * * @return TableHelper */ public function setCellHeaderFormat($cellHeaderFormat) { $this->table->getStyle()->setCellHeaderFormat($cellHeaderFormat); return $this; } /** * Sets row cell format. * * @param string $cellRowFormat * * @return TableHelper */ public function setCellRowFormat($cellRowFormat) { $this->table->getStyle()->setCellHeaderFormat($cellRowFormat); return $this; } /** * Sets row cell content format. * * @param string $cellRowContentFormat * * @return TableHelper */ public function setCellRowContentFormat($cellRowContentFormat) { $this->table->getStyle()->setCellRowContentFormat($cellRowContentFormat); return $this; } /** * Sets table border format. * * @param string $borderFormat * * @return TableHelper */ public function setBorderFormat($borderFormat) { $this->table->getStyle()->setBorderFormat($borderFormat); return $this; } /** * Sets cell padding type. * * @param int $padType STR_PAD_* * * @return TableHelper */ public function setPadType($padType) { $this->table->getStyle()->setPadType($padType); return $this; } /** * Renders table to output. * * Example: * +---------------+-----------------------+------------------+ * | ISBN | Title | Author | * +---------------+-----------------------+------------------+ * | 99921-58-10-7 | Divine Comedy | Dante Alighieri | * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | * +---------------+-----------------------+------------------+ * * @param OutputInterface $output */ public function render(OutputInterface $output) { $p = new \ReflectionProperty($this->table, 'output'); $p->setAccessible(true); $p->setValue($this->table, $output); $this->table->render(); } /** * {@inheritdoc} */ public function getName() { return 'table'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Helper; /** * Marks a row as being a separator. * * @author Fabien Potencier */ class TableSeparator extends TableCell { /** * @param array $options */ public function __construct(array $options = array()) { parent::__construct('', $options); } } * * 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\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Defines the styles for a Table. * * @author Fabien Potencier * @author Саша Стаменковић */ class TableStyle { private $paddingChar = ' '; private $horizontalBorderChar = '-'; private $verticalBorderChar = '|'; private $crossingChar = '+'; private $cellHeaderFormat = '%s'; private $cellRowFormat = '%s'; private $cellRowContentFormat = ' %s '; private $borderFormat = '%s'; private $padType = STR_PAD_RIGHT; /** * Sets padding character, used for cell padding. * * @param string $paddingChar * * @return TableStyle */ public function setPaddingChar($paddingChar) { if (!$paddingChar) { throw new LogicException('The padding char must not be empty'); } $this->paddingChar = $paddingChar; return $this; } /** * Gets padding character, used for cell padding. * * @return string */ public function getPaddingChar() { return $this->paddingChar; } /** * Sets horizontal border character. * * @param string $horizontalBorderChar * * @return TableStyle */ public function setHorizontalBorderChar($horizontalBorderChar) { $this->horizontalBorderChar = $horizontalBorderChar; return $this; } /** * Gets horizontal border character. * * @return string */ public function getHorizontalBorderChar() { return $this->horizontalBorderChar; } /** * Sets vertical border character. * * @param string $verticalBorderChar * * @return TableStyle */ public function setVerticalBorderChar($verticalBorderChar) { $this->verticalBorderChar = $verticalBorderChar; return $this; } /** * Gets vertical border character. * * @return string */ public function getVerticalBorderChar() { return $this->verticalBorderChar; } /** * Sets crossing character. * * @param string $crossingChar * * @return TableStyle */ public function setCrossingChar($crossingChar) { $this->crossingChar = $crossingChar; return $this; } /** * Gets crossing character. * * @return string $crossingChar */ public function getCrossingChar() { return $this->crossingChar; } /** * Sets header cell format. * * @param string $cellHeaderFormat * * @return TableStyle */ public function setCellHeaderFormat($cellHeaderFormat) { $this->cellHeaderFormat = $cellHeaderFormat; return $this; } /** * Gets header cell format. * * @return string */ public function getCellHeaderFormat() { return $this->cellHeaderFormat; } /** * Sets row cell format. * * @param string $cellRowFormat * * @return TableStyle */ public function setCellRowFormat($cellRowFormat) { $this->cellRowFormat = $cellRowFormat; return $this; } /** * Gets row cell format. * * @return string */ public function getCellRowFormat() { return $this->cellRowFormat; } /** * Sets row cell content format. * * @param string $cellRowContentFormat * * @return TableStyle */ public function setCellRowContentFormat($cellRowContentFormat) { $this->cellRowContentFormat = $cellRowContentFormat; return $this; } /** * Gets row cell content format. * * @return string */ public function getCellRowContentFormat() { return $this->cellRowContentFormat; } /** * Sets table border format. * * @param string $borderFormat * * @return TableStyle */ public function setBorderFormat($borderFormat) { $this->borderFormat = $borderFormat; return $this; } /** * Gets table border format. * * @return string */ public function getBorderFormat() { return $this->borderFormat; } /** * Sets cell padding type. * * @param int $padType STR_PAD_* * * @return TableStyle */ public function setPadType($padType) { if (!in_array($padType, array(STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH), true)) { throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).'); } $this->padType = $padType; return $this; } /** * Gets cell padding type. * * @return int */ public function getPadType() { return $this->padType; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\RuntimeException; /** * ArgvInput represents an input coming from the CLI arguments. * * Usage: * * $input = new ArgvInput(); * * By default, the `$_SERVER['argv']` array is used for the input values. * * This can be overridden by explicitly passing the input values in the constructor: * * $input = new ArgvInput($_SERVER['argv']); * * If you pass it yourself, don't forget that the first element of the array * is the name of the running application. * * When passing an argument to the constructor, be sure that it respects * the same rules as the argv one. It's almost always better to use the * `StringInput` when you want to provide your own input. * * @author Fabien Potencier * * @see http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html * @see http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_02 */ class ArgvInput extends Input { private $tokens; private $parsed; /** * Constructor. * * @param array|null $argv An array of parameters from the CLI (in the argv format) * @param InputDefinition|null $definition A InputDefinition instance */ public function __construct(array $argv = null, InputDefinition $definition = null) { if (null === $argv) { $argv = $_SERVER['argv']; } // strip the application name array_shift($argv); $this->tokens = $argv; parent::__construct($definition); } protected function setTokens(array $tokens) { $this->tokens = $tokens; } /** * {@inheritdoc} */ protected function parse() { $parseOptions = true; $this->parsed = $this->tokens; while (null !== $token = array_shift($this->parsed)) { if ($parseOptions && '' == $token) { $this->parseArgument($token); } elseif ($parseOptions && '--' == $token) { $parseOptions = false; } elseif ($parseOptions && 0 === strpos($token, '--')) { $this->parseLongOption($token); } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { $this->parseShortOption($token); } else { $this->parseArgument($token); } } } /** * Parses a short option. * * @param string $token The current token */ private function parseShortOption($token) { $name = substr($token, 1); if (strlen($name) > 1) { if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) { // an option with a value (with no space) $this->addShortOption($name[0], substr($name, 1)); } else { $this->parseShortOptionSet($name); } } else { $this->addShortOption($name, null); } } /** * Parses a short option set. * * @param string $name The current token * * @throws RuntimeException When option given doesn't exist */ private function parseShortOptionSet($name) { $len = strlen($name); for ($i = 0; $i < $len; ++$i) { if (!$this->definition->hasShortcut($name[$i])) { throw new RuntimeException(sprintf('The "-%s" option does not exist.', $name[$i])); } $option = $this->definition->getOptionForShortcut($name[$i]); if ($option->acceptValue()) { $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); break; } else { $this->addLongOption($option->getName(), null); } } } /** * Parses a long option. * * @param string $token The current token */ private function parseLongOption($token) { $name = substr($token, 2); if (false !== $pos = strpos($name, '=')) { if (0 === strlen($value = substr($name, $pos + 1))) { array_unshift($this->parsed, null); } $this->addLongOption(substr($name, 0, $pos), $value); } else { $this->addLongOption($name, null); } } /** * Parses an argument. * * @param string $token The current token * * @throws RuntimeException When too many arguments are given */ private function parseArgument($token) { $c = count($this->arguments); // if input is expecting another argument, add it if ($this->definition->hasArgument($c)) { $arg = $this->definition->getArgument($c); $this->arguments[$arg->getName()] = $arg->isArray() ? array($token) : $token; // if last argument isArray(), append token to last argument } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { $arg = $this->definition->getArgument($c - 1); $this->arguments[$arg->getName()][] = $token; // unexpected argument } else { $all = $this->definition->getArguments(); if (count($all)) { throw new RuntimeException(sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all)))); } throw new RuntimeException(sprintf('No arguments expected, got "%s".', $token)); } } /** * Adds a short option value. * * @param string $shortcut The short option key * @param mixed $value The value for the option * * @throws RuntimeException When option given doesn't exist */ private function addShortOption($shortcut, $value) { if (!$this->definition->hasShortcut($shortcut)) { throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); } $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); } /** * Adds a long option value. * * @param string $name The long option key * @param mixed $value The value for the option * * @throws RuntimeException When option given doesn't exist */ private function addLongOption($name, $value) { if (!$this->definition->hasOption($name)) { throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name)); } $option = $this->definition->getOption($name); // Convert empty values to null if (!isset($value[0])) { $value = null; } if (null !== $value && !$option->acceptValue()) { throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); } if (null === $value && $option->acceptValue() && count($this->parsed)) { // if option accepts an optional or mandatory argument // let's see if there is one provided $next = array_shift($this->parsed); if (isset($next[0]) && '-' !== $next[0]) { $value = $next; } elseif (empty($next)) { $value = null; } else { array_unshift($this->parsed, $next); } } if (null === $value) { if ($option->isValueRequired()) { throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name)); } if (!$option->isArray()) { $value = $option->isValueOptional() ? $option->getDefault() : true; } } if ($option->isArray()) { $this->options[$name][] = $value; } else { $this->options[$name] = $value; } } /** * {@inheritdoc} */ public function getFirstArgument() { foreach ($this->tokens as $token) { if ($token && '-' === $token[0]) { continue; } return $token; } } /** * {@inheritdoc} */ public function hasParameterOption($values) { $values = (array) $values; foreach ($this->tokens as $token) { foreach ($values as $value) { if ($token === $value || 0 === strpos($token, $value.'=')) { return true; } } } return false; } /** * {@inheritdoc} */ public function getParameterOption($values, $default = false) { $values = (array) $values; $tokens = $this->tokens; while (0 < count($tokens)) { $token = array_shift($tokens); foreach ($values as $value) { if ($token === $value || 0 === strpos($token, $value.'=')) { if (false !== $pos = strpos($token, '=')) { return substr($token, $pos + 1); } return array_shift($tokens); } } } return $default; } /** * Returns a stringified representation of the args passed to the command. * * @return string */ public function __toString() { $self = $this; $tokens = array_map(function ($token) use ($self) { if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { return $match[1].$self->escapeToken($match[2]); } if ($token && $token[0] !== '-') { return $self->escapeToken($token); } return $token; }, $this->tokens); return implode(' ', $tokens); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\InvalidOptionException; /** * ArrayInput represents an input provided as an array. * * Usage: * * $input = new ArrayInput(array('name' => 'foo', '--bar' => 'foobar')); * * @author Fabien Potencier */ class ArrayInput extends Input { private $parameters; /** * Constructor. * * @param array $parameters An array of parameters * @param InputDefinition|null $definition A InputDefinition instance */ public function __construct(array $parameters, InputDefinition $definition = null) { $this->parameters = $parameters; parent::__construct($definition); } /** * {@inheritdoc} */ public function getFirstArgument() { foreach ($this->parameters as $key => $value) { if ($key && '-' === $key[0]) { continue; } return $value; } } /** * {@inheritdoc} */ public function hasParameterOption($values) { $values = (array) $values; foreach ($this->parameters as $k => $v) { if (!is_int($k)) { $v = $k; } if (in_array($v, $values)) { return true; } } return false; } /** * {@inheritdoc} */ public function getParameterOption($values, $default = false) { $values = (array) $values; foreach ($this->parameters as $k => $v) { if (is_int($k)) { if (in_array($v, $values)) { return true; } } elseif (in_array($k, $values)) { return $v; } } return $default; } /** * Returns a stringified representation of the args passed to the command. * * @return string */ public function __toString() { $params = array(); foreach ($this->parameters as $param => $val) { if ($param && '-' === $param[0]) { $params[] = $param.('' != $val ? '='.$this->escapeToken($val) : ''); } else { $params[] = $this->escapeToken($val); } } return implode(' ', $params); } /** * {@inheritdoc} */ protected function parse() { foreach ($this->parameters as $key => $value) { if (0 === strpos($key, '--')) { $this->addLongOption(substr($key, 2), $value); } elseif ('-' === $key[0]) { $this->addShortOption(substr($key, 1), $value); } else { $this->addArgument($key, $value); } } } /** * Adds a short option value. * * @param string $shortcut The short option key * @param mixed $value The value for the option * * @throws InvalidOptionException When option given doesn't exist */ private function addShortOption($shortcut, $value) { if (!$this->definition->hasShortcut($shortcut)) { throw new InvalidOptionException(sprintf('The "-%s" option does not exist.', $shortcut)); } $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); } /** * Adds a long option value. * * @param string $name The long option key * @param mixed $value The value for the option * * @throws InvalidOptionException When option given doesn't exist * @throws InvalidOptionException When a required value is missing */ private function addLongOption($name, $value) { if (!$this->definition->hasOption($name)) { throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name)); } $option = $this->definition->getOption($name); if (null === $value) { if ($option->isValueRequired()) { throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $name)); } $value = $option->isValueOptional() ? $option->getDefault() : true; } $this->options[$name] = $value; } /** * Adds an argument value. * * @param string $name The argument name * @param mixed $value The value for the argument * * @throws InvalidArgumentException When argument given doesn't exist */ private function addArgument($name, $value) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } $this->arguments[$name] = $value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; /** * Input is the base class for all concrete Input classes. * * Three concrete classes are provided by default: * * * `ArgvInput`: The input comes from the CLI arguments (argv) * * `StringInput`: The input is provided as a string * * `ArrayInput`: The input is provided as an array * * @author Fabien Potencier */ abstract class Input implements InputInterface { /** * @var InputDefinition */ protected $definition; protected $options = array(); protected $arguments = array(); protected $interactive = true; /** * Constructor. * * @param InputDefinition|null $definition A InputDefinition instance */ public function __construct(InputDefinition $definition = null) { if (null === $definition) { $this->definition = new InputDefinition(); } else { $this->bind($definition); $this->validate(); } } /** * {@inheritdoc} */ public function bind(InputDefinition $definition) { $this->arguments = array(); $this->options = array(); $this->definition = $definition; $this->parse(); } /** * Processes command line arguments. */ abstract protected function parse(); /** * {@inheritdoc} */ public function validate() { $definition = $this->definition; $givenArguments = $this->arguments; $missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) { return !array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired(); }); if (count($missingArguments) > 0) { throw new RuntimeException(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments))); } } /** * {@inheritdoc} */ public function isInteractive() { return $this->interactive; } /** * {@inheritdoc} */ public function setInteractive($interactive) { $this->interactive = (bool) $interactive; } /** * {@inheritdoc} */ public function getArguments() { return array_merge($this->definition->getArgumentDefaults(), $this->arguments); } /** * {@inheritdoc} */ public function getArgument($name) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } return isset($this->arguments[$name]) ? $this->arguments[$name] : $this->definition->getArgument($name)->getDefault(); } /** * {@inheritdoc} */ public function setArgument($name, $value) { if (!$this->definition->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } $this->arguments[$name] = $value; } /** * {@inheritdoc} */ public function hasArgument($name) { return $this->definition->hasArgument($name); } /** * {@inheritdoc} */ public function getOptions() { return array_merge($this->definition->getOptionDefaults(), $this->options); } /** * {@inheritdoc} */ public function getOption($name) { if (!$this->definition->hasOption($name)) { throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); } return isset($this->options[$name]) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); } /** * {@inheritdoc} */ public function setOption($name, $value) { if (!$this->definition->hasOption($name)) { throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); } $this->options[$name] = $value; } /** * {@inheritdoc} */ public function hasOption($name) { return $this->definition->hasOption($name); } /** * Escapes a token through escapeshellarg if it contains unsafe chars. * * @param string $token * * @return string */ public function escapeToken($token) { return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Represents a command line argument. * * @author Fabien Potencier */ class InputArgument { const REQUIRED = 1; const OPTIONAL = 2; const IS_ARRAY = 4; private $name; private $mode; private $default; private $description; /** * Constructor. * * @param string $name The argument name * @param int $mode The argument mode: self::REQUIRED or self::OPTIONAL * @param string $description A description text * @param mixed $default The default value (for self::OPTIONAL mode only) * * @throws InvalidArgumentException When argument mode is not valid */ public function __construct($name, $mode = null, $description = '', $default = null) { if (null === $mode) { $mode = self::OPTIONAL; } elseif (!is_int($mode) || $mode > 7 || $mode < 1) { throw new InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); } $this->name = $name; $this->mode = $mode; $this->description = $description; $this->setDefault($default); } /** * Returns the argument name. * * @return string The argument name */ public function getName() { return $this->name; } /** * Returns true if the argument is required. * * @return bool true if parameter mode is self::REQUIRED, false otherwise */ public function isRequired() { return self::REQUIRED === (self::REQUIRED & $this->mode); } /** * Returns true if the argument can take multiple values. * * @return bool true if mode is self::IS_ARRAY, false otherwise */ public function isArray() { return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); } /** * Sets the default value. * * @param mixed $default The default value * * @throws LogicException When incorrect default value is given */ public function setDefault($default = null) { if (self::REQUIRED === $this->mode && null !== $default) { throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); } if ($this->isArray()) { if (null === $default) { $default = array(); } elseif (!is_array($default)) { throw new LogicException('A default value for an array argument must be an array.'); } } $this->default = $default; } /** * Returns the default value. * * @return mixed The default value */ public function getDefault() { return $this->default; } /** * Returns the description text. * * @return string The description text */ public function getDescription() { return $this->description; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; /** * InputAwareInterface should be implemented by classes that depends on the * Console Input. * * @author Wouter J */ interface InputAwareInterface { /** * Sets the Console Input. * * @param InputInterface */ public function setInput(InputInterface $input); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Descriptor\TextDescriptor; use Symfony\Component\Console\Descriptor\XmlDescriptor; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * A InputDefinition represents a set of valid command line arguments and options. * * Usage: * * $definition = new InputDefinition(array( * new InputArgument('name', InputArgument::REQUIRED), * new InputOption('foo', 'f', InputOption::VALUE_REQUIRED), * )); * * @author Fabien Potencier */ class InputDefinition { private $arguments; private $requiredCount; private $hasAnArrayArgument = false; private $hasOptional; private $options; private $shortcuts; /** * Constructor. * * @param array $definition An array of InputArgument and InputOption instance */ public function __construct(array $definition = array()) { $this->setDefinition($definition); } /** * Sets the definition of the input. * * @param array $definition The definition array */ public function setDefinition(array $definition) { $arguments = array(); $options = array(); foreach ($definition as $item) { if ($item instanceof InputOption) { $options[] = $item; } else { $arguments[] = $item; } } $this->setArguments($arguments); $this->setOptions($options); } /** * Sets the InputArgument objects. * * @param InputArgument[] $arguments An array of InputArgument objects */ public function setArguments($arguments = array()) { $this->arguments = array(); $this->requiredCount = 0; $this->hasOptional = false; $this->hasAnArrayArgument = false; $this->addArguments($arguments); } /** * Adds an array of InputArgument objects. * * @param InputArgument[] $arguments An array of InputArgument objects */ public function addArguments($arguments = array()) { if (null !== $arguments) { foreach ($arguments as $argument) { $this->addArgument($argument); } } } /** * Adds an InputArgument object. * * @param InputArgument $argument An InputArgument object * * @throws LogicException When incorrect argument is given */ public function addArgument(InputArgument $argument) { if (isset($this->arguments[$argument->getName()])) { throw new LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); } if ($this->hasAnArrayArgument) { throw new LogicException('Cannot add an argument after an array argument.'); } if ($argument->isRequired() && $this->hasOptional) { throw new LogicException('Cannot add a required argument after an optional one.'); } if ($argument->isArray()) { $this->hasAnArrayArgument = true; } if ($argument->isRequired()) { ++$this->requiredCount; } else { $this->hasOptional = true; } $this->arguments[$argument->getName()] = $argument; } /** * Returns an InputArgument by name or by position. * * @param string|int $name The InputArgument name or position * * @return InputArgument An InputArgument object * * @throws InvalidArgumentException When argument given doesn't exist */ public function getArgument($name) { if (!$this->hasArgument($name)) { throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); } $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; return $arguments[$name]; } /** * Returns true if an InputArgument object exists by name or position. * * @param string|int $name The InputArgument name or position * * @return bool true if the InputArgument object exists, false otherwise */ public function hasArgument($name) { $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments; return isset($arguments[$name]); } /** * Gets the array of InputArgument objects. * * @return InputArgument[] An array of InputArgument objects */ public function getArguments() { return $this->arguments; } /** * Returns the number of InputArguments. * * @return int The number of InputArguments */ public function getArgumentCount() { return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments); } /** * Returns the number of required InputArguments. * * @return int The number of required InputArguments */ public function getArgumentRequiredCount() { return $this->requiredCount; } /** * Gets the default values. * * @return array An array of default values */ public function getArgumentDefaults() { $values = array(); foreach ($this->arguments as $argument) { $values[$argument->getName()] = $argument->getDefault(); } return $values; } /** * Sets the InputOption objects. * * @param InputOption[] $options An array of InputOption objects */ public function setOptions($options = array()) { $this->options = array(); $this->shortcuts = array(); $this->addOptions($options); } /** * Adds an array of InputOption objects. * * @param InputOption[] $options An array of InputOption objects */ public function addOptions($options = array()) { foreach ($options as $option) { $this->addOption($option); } } /** * Adds an InputOption object. * * @param InputOption $option An InputOption object * * @throws LogicException When option given already exist */ public function addOption(InputOption $option) { if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName())); } if ($option->getShortcut()) { foreach (explode('|', $option->getShortcut()) as $shortcut) { if (isset($this->shortcuts[$shortcut]) && !$option->equals($this->options[$this->shortcuts[$shortcut]])) { throw new LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut)); } } } $this->options[$option->getName()] = $option; if ($option->getShortcut()) { foreach (explode('|', $option->getShortcut()) as $shortcut) { $this->shortcuts[$shortcut] = $option->getName(); } } } /** * Returns an InputOption by name. * * @param string $name The InputOption name * * @return InputOption A InputOption object * * @throws InvalidArgumentException When option given doesn't exist */ public function getOption($name) { if (!$this->hasOption($name)) { throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); } return $this->options[$name]; } /** * Returns true if an InputOption object exists by name. * * This method can't be used to check if the user included the option when * executing the command (use getOption() instead). * * @param string $name The InputOption name * * @return bool true if the InputOption object exists, false otherwise */ public function hasOption($name) { return isset($this->options[$name]); } /** * Gets the array of InputOption objects. * * @return InputOption[] An array of InputOption objects */ public function getOptions() { return $this->options; } /** * Returns true if an InputOption object exists by shortcut. * * @param string $name The InputOption shortcut * * @return bool true if the InputOption object exists, false otherwise */ public function hasShortcut($name) { return isset($this->shortcuts[$name]); } /** * Gets an InputOption by shortcut. * * @param string $shortcut the Shortcut name * * @return InputOption An InputOption object */ public function getOptionForShortcut($shortcut) { return $this->getOption($this->shortcutToName($shortcut)); } /** * Gets an array of default values. * * @return array An array of all default values */ public function getOptionDefaults() { $values = array(); foreach ($this->options as $option) { $values[$option->getName()] = $option->getDefault(); } return $values; } /** * Returns the InputOption name given a shortcut. * * @param string $shortcut The shortcut * * @return string The InputOption name * * @throws InvalidArgumentException When option given does not exist */ private function shortcutToName($shortcut) { if (!isset($this->shortcuts[$shortcut])) { throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); } return $this->shortcuts[$shortcut]; } /** * Gets the synopsis. * * @param bool $short Whether to return the short version (with options folded) or not * * @return string The synopsis */ public function getSynopsis($short = false) { $elements = array(); if ($short && $this->getOptions()) { $elements[] = '[options]'; } elseif (!$short) { foreach ($this->getOptions() as $option) { $value = ''; if ($option->acceptValue()) { $value = sprintf( ' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : '' ); } $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value); } } if (count($elements) && $this->getArguments()) { $elements[] = '[--]'; } foreach ($this->getArguments() as $argument) { $element = '<'.$argument->getName().'>'; if (!$argument->isRequired()) { $element = '['.$element.']'; } elseif ($argument->isArray()) { $element = $element.' ('.$element.')'; } if ($argument->isArray()) { $element .= '...'; } $elements[] = $element; } return implode(' ', $elements); } /** * Returns a textual representation of the InputDefinition. * * @return string A string representing the InputDefinition * * @deprecated since version 2.3, to be removed in 3.0. */ public function asText() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); $descriptor = new TextDescriptor(); $output = new BufferedOutput(BufferedOutput::VERBOSITY_NORMAL, true); $descriptor->describe($output, $this, array('raw_output' => true)); return $output->fetch(); } /** * Returns an XML representation of the InputDefinition. * * @param bool $asDom Whether to return a DOM or an XML string * * @return string|\DOMDocument An XML string representing the InputDefinition * * @deprecated since version 2.3, to be removed in 3.0. */ public function asXml($asDom = false) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.3 and will be removed in 3.0.', E_USER_DEPRECATED); $descriptor = new XmlDescriptor(); if ($asDom) { return $descriptor->getInputDefinitionDocument($this); } $output = new BufferedOutput(); $descriptor->describe($output, $this); return $output->fetch(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; /** * InputInterface is the interface implemented by all input classes. * * @author Fabien Potencier */ interface InputInterface { /** * Returns the first argument from the raw parameters (not parsed). * * @return string The value of the first argument or null otherwise */ public function getFirstArgument(); /** * Returns true if the raw parameters (not parsed) contain a value. * * This method is to be used to introspect the input parameters * before they have been validated. It must be used carefully. * * @param string|array $values The values to look for in the raw parameters (can be an array) * * @return bool true if the value is contained in the raw parameters */ public function hasParameterOption($values); /** * Returns the value of a raw option (not parsed). * * This method is to be used to introspect the input parameters * before they have been validated. It must be used carefully. * * @param string|array $values The value(s) to look for in the raw parameters (can be an array) * @param mixed $default The default value to return if no result is found * * @return mixed The option value */ public function getParameterOption($values, $default = false); /** * Binds the current Input instance with the given arguments and options. * * @param InputDefinition $definition A InputDefinition instance */ public function bind(InputDefinition $definition); /** * Validates the input. * * @throws RuntimeException When not enough arguments are given */ public function validate(); /** * Returns all the given arguments merged with the default values. * * @return array */ public function getArguments(); /** * Returns the argument value for a given argument name. * * @param string $name The argument name * * @return mixed The argument value * * @throws InvalidArgumentException When argument given doesn't exist */ public function getArgument($name); /** * Sets an argument value by name. * * @param string $name The argument name * @param string $value The argument value * * @throws InvalidArgumentException When argument given doesn't exist */ public function setArgument($name, $value); /** * Returns true if an InputArgument object exists by name or position. * * @param string|int $name The InputArgument name or position * * @return bool true if the InputArgument object exists, false otherwise */ public function hasArgument($name); /** * Returns all the given options merged with the default values. * * @return array */ public function getOptions(); /** * Returns the option value for a given option name. * * @param string $name The option name * * @return mixed The option value * * @throws InvalidArgumentException When option given doesn't exist */ public function getOption($name); /** * Sets an option value by name. * * @param string $name The option name * @param string|bool $value The option value * * @throws InvalidArgumentException When option given doesn't exist */ public function setOption($name, $value); /** * Returns true if an InputOption object exists by name. * * @param string $name The InputOption name * * @return bool true if the InputOption object exists, false otherwise */ public function hasOption($name); /** * Is this input means interactive? * * @return bool */ public function isInteractive(); /** * Sets the input interactivity. * * @param bool $interactive If the input should be interactive */ public function setInteractive($interactive); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Represents a command line option. * * @author Fabien Potencier */ class InputOption { const VALUE_NONE = 1; const VALUE_REQUIRED = 2; const VALUE_OPTIONAL = 4; const VALUE_IS_ARRAY = 8; private $name; private $shortcut; private $mode; private $default; private $description; /** * Constructor. * * @param string $name The option name * @param string|array $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts * @param int $mode The option mode: One of the VALUE_* constants * @param string $description A description text * @param mixed $default The default value (must be null for self::VALUE_NONE) * * @throws InvalidArgumentException If option mode is invalid or incompatible */ public function __construct($name, $shortcut = null, $mode = null, $description = '', $default = null) { if (0 === strpos($name, '--')) { $name = substr($name, 2); } if (empty($name)) { throw new InvalidArgumentException('An option name cannot be empty.'); } if (empty($shortcut)) { $shortcut = null; } if (null !== $shortcut) { if (is_array($shortcut)) { $shortcut = implode('|', $shortcut); } $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); $shortcuts = array_filter($shortcuts); $shortcut = implode('|', $shortcuts); if (empty($shortcut)) { throw new InvalidArgumentException('An option shortcut cannot be empty.'); } } if (null === $mode) { $mode = self::VALUE_NONE; } elseif (!is_int($mode) || $mode > 15 || $mode < 1) { throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); } $this->name = $name; $this->shortcut = $shortcut; $this->mode = $mode; $this->description = $description; if ($this->isArray() && !$this->acceptValue()) { throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); } $this->setDefault($default); } /** * Returns the option shortcut. * * @return string The shortcut */ public function getShortcut() { return $this->shortcut; } /** * Returns the option name. * * @return string The name */ public function getName() { return $this->name; } /** * Returns true if the option accepts a value. * * @return bool true if value mode is not self::VALUE_NONE, false otherwise */ public function acceptValue() { return $this->isValueRequired() || $this->isValueOptional(); } /** * Returns true if the option requires a value. * * @return bool true if value mode is self::VALUE_REQUIRED, false otherwise */ public function isValueRequired() { return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); } /** * Returns true if the option takes an optional value. * * @return bool true if value mode is self::VALUE_OPTIONAL, false otherwise */ public function isValueOptional() { return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); } /** * Returns true if the option can take multiple values. * * @return bool true if mode is self::VALUE_IS_ARRAY, false otherwise */ public function isArray() { return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); } /** * Sets the default value. * * @param mixed $default The default value * * @throws LogicException When incorrect default value is given */ public function setDefault($default = null) { if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); } if ($this->isArray()) { if (null === $default) { $default = array(); } elseif (!is_array($default)) { throw new LogicException('A default value for an array option must be an array.'); } } $this->default = $this->acceptValue() ? $default : false; } /** * Returns the default value. * * @return mixed The default value */ public function getDefault() { return $this->default; } /** * Returns the description text. * * @return string The description text */ public function getDescription() { return $this->description; } /** * Checks whether the given option equals this one. * * @param InputOption $option option to compare * * @return bool */ public function equals(InputOption $option) { return $option->getName() === $this->getName() && $option->getShortcut() === $this->getShortcut() && $option->getDefault() === $this->getDefault() && $option->isArray() === $this->isArray() && $option->isValueRequired() === $this->isValueRequired() && $option->isValueOptional() === $this->isValueOptional() ; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Input; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * StringInput represents an input provided as a string. * * Usage: * * $input = new StringInput('foo --bar="foobar"'); * * @author Fabien Potencier */ class StringInput extends ArgvInput { const REGEX_STRING = '([^\s]+?)(?:\s|(?setTokens($this->tokenize($input)); if (null !== $definition) { $this->bind($definition); } } /** * Tokenizes a string. * * @param string $input The input to tokenize * * @return array An array of tokens * * @throws InvalidArgumentException When unable to parse input (should never happen) */ private function tokenize($input) { $tokens = array(); $length = strlen($input); $cursor = 0; while ($cursor < $length) { if (preg_match('/\s+/A', $input, $match, null, $cursor)) { } elseif (preg_match('/([^="\'\s]+?)(=?)('.self::REGEX_QUOTED_STRING.'+)/A', $input, $match, null, $cursor)) { $tokens[] = $match[1].$match[2].stripcslashes(str_replace(array('"\'', '\'"', '\'\'', '""'), '', substr($match[3], 1, strlen($match[3]) - 2))); } elseif (preg_match('/'.self::REGEX_QUOTED_STRING.'/A', $input, $match, null, $cursor)) { $tokens[] = stripcslashes(substr($match[0], 1, strlen($match[0]) - 2)); } elseif (preg_match('/'.self::REGEX_STRING.'/A', $input, $match, null, $cursor)) { $tokens[] = stripcslashes($match[1]); } else { // should never happen throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ..."', substr($input, $cursor, 10))); } $cursor += strlen($match[0]); } return $tokens; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Logger; use Psr\Log\AbstractLogger; use Psr\Log\InvalidArgumentException; use Psr\Log\LogLevel; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; /** * PSR-3 compliant console logger. * * @author Kévin Dunglas * * @see http://www.php-fig.org/psr/psr-3/ */ class ConsoleLogger extends AbstractLogger { const INFO = 'info'; const ERROR = 'error'; /** * @var OutputInterface */ private $output; /** * @var array */ private $verbosityLevelMap = array( LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL, LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL, LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL, LogLevel::NOTICE => OutputInterface::VERBOSITY_VERBOSE, LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE, LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG, ); /** * @var array */ private $formatLevelMap = array( LogLevel::EMERGENCY => self::ERROR, LogLevel::ALERT => self::ERROR, LogLevel::CRITICAL => self::ERROR, LogLevel::ERROR => self::ERROR, LogLevel::WARNING => self::INFO, LogLevel::NOTICE => self::INFO, LogLevel::INFO => self::INFO, LogLevel::DEBUG => self::INFO, ); /** * @param OutputInterface $output * @param array $verbosityLevelMap * @param array $formatLevelMap */ public function __construct(OutputInterface $output, array $verbosityLevelMap = array(), array $formatLevelMap = array()) { $this->output = $output; $this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap; $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap; } /** * {@inheritdoc} */ public function log($level, $message, array $context = array()) { if (!isset($this->verbosityLevelMap[$level])) { throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); } // Write to the error output if necessary and available if ($this->formatLevelMap[$level] === self::ERROR && $this->output instanceof ConsoleOutputInterface) { $output = $this->output->getErrorOutput(); } else { $output = $this->output; } if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) { $output->writeln(sprintf('<%1$s>[%2$s] %3$s', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context))); } } /** * Interpolates context values into the message placeholders. * * @author PHP Framework Interoperability Group * * @param string $message * @param array $context * * @return string */ private function interpolate($message, array $context) { // build a replacement array with braces around the context keys $replace = array(); foreach ($context as $key => $val) { if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { $replace[sprintf('{%s}', $key)] = $val; } } // interpolate replacement values into the message and return return strtr($message, $replace); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; /** * @author Jean-François Simon */ class BufferedOutput extends Output { /** * @var string */ private $buffer = ''; /** * Empties buffer and returns its content. * * @return string */ public function fetch() { $content = $this->buffer; $this->buffer = ''; return $content; } /** * {@inheritdoc} */ protected function doWrite($message, $newline) { $this->buffer .= $message; if ($newline) { $this->buffer .= "\n"; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * ConsoleOutput is the default class for all CLI output. It uses STDOUT. * * This class is a convenient wrapper around `StreamOutput`. * * $output = new ConsoleOutput(); * * This is equivalent to: * * $output = new StreamOutput(fopen('php://stdout', 'w')); * * @author Fabien Potencier */ class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface { /** * @var StreamOutput */ private $stderr; /** * Constructor. * * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) */ public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null) { parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter); $actualDecorated = $this->isDecorated(); $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter()); if (null === $decorated) { $this->setDecorated($actualDecorated && $this->stderr->isDecorated()); } } /** * {@inheritdoc} */ public function setDecorated($decorated) { parent::setDecorated($decorated); $this->stderr->setDecorated($decorated); } /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { parent::setFormatter($formatter); $this->stderr->setFormatter($formatter); } /** * {@inheritdoc} */ public function setVerbosity($level) { parent::setVerbosity($level); $this->stderr->setVerbosity($level); } /** * {@inheritdoc} */ public function getErrorOutput() { return $this->stderr; } /** * {@inheritdoc} */ public function setErrorOutput(OutputInterface $error) { $this->stderr = $error; } /** * Returns true if current environment supports writing console output to * STDOUT. * * @return bool */ protected function hasStdoutSupport() { return false === $this->isRunningOS400(); } /** * Returns true if current environment supports writing console output to * STDERR. * * @return bool */ protected function hasStderrSupport() { return false === $this->isRunningOS400(); } /** * Checks if current executing environment is IBM iSeries (OS400), which * doesn't properly convert character-encodings between ASCII to EBCDIC. * * @return bool */ private function isRunningOS400() { $checks = array( function_exists('php_uname') ? php_uname('s') : '', getenv('OSTYPE'), PHP_OS, ); return false !== stripos(implode(';', $checks), 'OS400'); } /** * @return resource */ private function openOutputStream() { $outputStream = $this->hasStdoutSupport() ? 'php://stdout' : 'php://output'; return @fopen($outputStream, 'w') ?: fopen('php://output', 'w'); } /** * @return resource */ private function openErrorStream() { $errorStream = $this->hasStderrSupport() ? 'php://stderr' : 'php://output'; return fopen($errorStream, 'w'); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; /** * ConsoleOutputInterface is the interface implemented by ConsoleOutput class. * This adds information about stderr output stream. * * @author Dariusz Górecki */ interface ConsoleOutputInterface extends OutputInterface { /** * Gets the OutputInterface for errors. * * @return OutputInterface */ public function getErrorOutput(); /** * Sets the OutputInterface used for errors. * * @param OutputInterface $error */ public function setErrorOutput(OutputInterface $error); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * NullOutput suppresses all output. * * $output = new NullOutput(); * * @author Fabien Potencier * @author Tobias Schultze */ class NullOutput implements OutputInterface { /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { // do nothing } /** * {@inheritdoc} */ public function getFormatter() { // to comply with the interface we must return a OutputFormatterInterface return new OutputFormatter(); } /** * {@inheritdoc} */ public function setDecorated($decorated) { // do nothing } /** * {@inheritdoc} */ public function isDecorated() { return false; } /** * {@inheritdoc} */ public function setVerbosity($level) { // do nothing } /** * {@inheritdoc} */ public function getVerbosity() { return self::VERBOSITY_QUIET; } /** * {@inheritdoc} */ public function isQuiet() { return true; } /** * {@inheritdoc} */ public function isVerbose() { return false; } /** * {@inheritdoc} */ public function isVeryVerbose() { return false; } /** * {@inheritdoc} */ public function isDebug() { return false; } /** * {@inheritdoc} */ public function writeln($messages, $options = self::OUTPUT_NORMAL) { // do nothing } /** * {@inheritdoc} */ public function write($messages, $newline = false, $options = self::OUTPUT_NORMAL) { // do nothing } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Formatter\OutputFormatter; /** * Base class for output classes. * * There are five levels of verbosity: * * * normal: no option passed (normal output) * * verbose: -v (more output) * * very verbose: -vv (highly extended output) * * debug: -vvv (all debug output) * * quiet: -q (no output) * * @author Fabien Potencier */ abstract class Output implements OutputInterface { private $verbosity; private $formatter; /** * Constructor. * * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) * @param bool $decorated Whether to decorate messages * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) */ public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = false, OutputFormatterInterface $formatter = null) { $this->verbosity = null === $verbosity ? self::VERBOSITY_NORMAL : $verbosity; $this->formatter = $formatter ?: new OutputFormatter(); $this->formatter->setDecorated($decorated); } /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { $this->formatter = $formatter; } /** * {@inheritdoc} */ public function getFormatter() { return $this->formatter; } /** * {@inheritdoc} */ public function setDecorated($decorated) { $this->formatter->setDecorated($decorated); } /** * {@inheritdoc} */ public function isDecorated() { return $this->formatter->isDecorated(); } /** * {@inheritdoc} */ public function setVerbosity($level) { $this->verbosity = (int) $level; } /** * {@inheritdoc} */ public function getVerbosity() { return $this->verbosity; } /** * {@inheritdoc} */ public function isQuiet() { return self::VERBOSITY_QUIET === $this->verbosity; } /** * {@inheritdoc} */ public function isVerbose() { return self::VERBOSITY_VERBOSE <= $this->verbosity; } /** * {@inheritdoc} */ public function isVeryVerbose() { return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; } /** * {@inheritdoc} */ public function isDebug() { return self::VERBOSITY_DEBUG <= $this->verbosity; } /** * {@inheritdoc} */ public function writeln($messages, $options = self::OUTPUT_NORMAL) { $this->write($messages, true, $options); } /** * {@inheritdoc} */ public function write($messages, $newline = false, $options = self::OUTPUT_NORMAL) { $messages = (array) $messages; $types = self::OUTPUT_NORMAL | self::OUTPUT_RAW | self::OUTPUT_PLAIN; $type = $types & $options ?: self::OUTPUT_NORMAL; $verbosities = self::VERBOSITY_QUIET | self::VERBOSITY_NORMAL | self::VERBOSITY_VERBOSE | self::VERBOSITY_VERY_VERBOSE | self::VERBOSITY_DEBUG; $verbosity = $verbosities & $options ?: self::VERBOSITY_NORMAL; if ($verbosity > $this->getVerbosity()) { return; } foreach ($messages as $message) { switch ($type) { case OutputInterface::OUTPUT_NORMAL: $message = $this->formatter->format($message); break; case OutputInterface::OUTPUT_RAW: break; case OutputInterface::OUTPUT_PLAIN: $message = strip_tags($this->formatter->format($message)); break; } $this->doWrite($message, $newline); } } /** * Writes a message to the output. * * @param string $message A message to write to the output * @param bool $newline Whether to add a newline or not */ abstract protected function doWrite($message, $newline); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * OutputInterface is the interface implemented by all Output classes. * * @author Fabien Potencier */ interface OutputInterface { const VERBOSITY_QUIET = 16; const VERBOSITY_NORMAL = 32; const VERBOSITY_VERBOSE = 64; const VERBOSITY_VERY_VERBOSE = 128; const VERBOSITY_DEBUG = 256; const OUTPUT_NORMAL = 1; const OUTPUT_RAW = 2; const OUTPUT_PLAIN = 4; /** * Writes a message to the output. * * @param string|array $messages The message as an array of lines or a single string * @param bool $newline Whether to add a newline * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL */ public function write($messages, $newline = false, $options = 0); /** * Writes a message to the output and adds a newline at the end. * * @param string|array $messages The message as an array of lines of a single string * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL */ public function writeln($messages, $options = 0); /** * Sets the verbosity of the output. * * @param int $level The level of verbosity (one of the VERBOSITY constants) */ public function setVerbosity($level); /** * Gets the current verbosity of the output. * * @return int The current level of verbosity (one of the VERBOSITY constants) */ public function getVerbosity(); /** * Sets the decorated flag. * * @param bool $decorated Whether to decorate the messages */ public function setDecorated($decorated); /** * Gets the decorated flag. * * @return bool true if the output will decorate messages, false otherwise */ public function isDecorated(); /** * Sets output formatter. * * @param OutputFormatterInterface $formatter */ public function setFormatter(OutputFormatterInterface $formatter); /** * Returns current output formatter instance. * * @return OutputFormatterInterface */ public function getFormatter(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Output; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatterInterface; /** * StreamOutput writes the output to a given stream. * * Usage: * * $output = new StreamOutput(fopen('php://stdout', 'w')); * * As `StreamOutput` can use any stream, you can also use a file: * * $output = new StreamOutput(fopen('/path/to/output.log', 'a', false)); * * @author Fabien Potencier */ class StreamOutput extends Output { private $stream; /** * Constructor. * * @param resource $stream A stream resource * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) * * @throws InvalidArgumentException When first argument is not a real stream */ public function __construct($stream, $verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null) { if (!is_resource($stream) || 'stream' !== get_resource_type($stream)) { throw new InvalidArgumentException('The StreamOutput class needs a stream as its first argument.'); } $this->stream = $stream; if (null === $decorated) { $decorated = $this->hasColorSupport(); } parent::__construct($verbosity, $decorated, $formatter); } /** * Gets the stream attached to this StreamOutput instance. * * @return resource A stream resource */ public function getStream() { return $this->stream; } /** * {@inheritdoc} */ protected function doWrite($message, $newline) { if (false === @fwrite($this->stream, $message) || ($newline && (false === @fwrite($this->stream, PHP_EOL)))) { // should never happen throw new RuntimeException('Unable to write output.'); } fflush($this->stream); } /** * Returns true if the stream supports colorization. * * Colorization is disabled if not supported by the stream: * * - Windows != 10.0.10586 without Ansicon, ConEmu or Mintty * - non tty consoles * * @return bool true if the stream supports colorization, false otherwise */ protected function hasColorSupport() { if (DIRECTORY_SEPARATOR === '\\') { return '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR.'.'.PHP_WINDOWS_VERSION_MINOR.'.'.PHP_WINDOWS_VERSION_BUILD || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); } return function_exists('posix_isatty') && @posix_isatty($this->stream); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Question; use Symfony\Component\Console\Exception\InvalidArgumentException; /** * Represents a choice question. * * @author Fabien Potencier */ class ChoiceQuestion extends Question { private $choices; private $multiselect = false; private $prompt = ' > '; private $errorMessage = 'Value "%s" is invalid'; /** * Constructor. * * @param string $question The question to ask to the user * @param array $choices The list of available choices * @param mixed $default The default answer to return */ public function __construct($question, array $choices, $default = null) { parent::__construct($question, $default); $this->choices = $choices; $this->setValidator($this->getDefaultValidator()); $this->setAutocompleterValues($choices); } /** * Returns available choices. * * @return array */ public function getChoices() { return $this->choices; } /** * Sets multiselect option. * * When multiselect is set to true, multiple choices can be answered. * * @param bool $multiselect * * @return ChoiceQuestion The current instance */ public function setMultiselect($multiselect) { $this->multiselect = $multiselect; $this->setValidator($this->getDefaultValidator()); return $this; } /** * Returns whether the choices are multiselect. * * @return bool */ public function isMultiselect() { return $this->multiselect; } /** * Gets the prompt for choices. * * @return string */ public function getPrompt() { return $this->prompt; } /** * Sets the prompt for choices. * * @param string $prompt * * @return ChoiceQuestion The current instance */ public function setPrompt($prompt) { $this->prompt = $prompt; return $this; } /** * Sets the error message for invalid values. * * The error message has a string placeholder (%s) for the invalid value. * * @param string $errorMessage * * @return ChoiceQuestion The current instance */ public function setErrorMessage($errorMessage) { $this->errorMessage = $errorMessage; $this->setValidator($this->getDefaultValidator()); return $this; } /** * Returns the default answer validator. * * @return callable */ private function getDefaultValidator() { $choices = $this->choices; $errorMessage = $this->errorMessage; $multiselect = $this->multiselect; $isAssoc = $this->isAssoc($choices); return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { // Collapse all spaces. $selectedChoices = str_replace(' ', '', $selected); if ($multiselect) { // Check for a separated comma values if (!preg_match('/^[a-zA-Z0-9_-]+(?:,[a-zA-Z0-9_-]+)*$/', $selectedChoices, $matches)) { throw new InvalidArgumentException(sprintf($errorMessage, $selected)); } $selectedChoices = explode(',', $selectedChoices); } else { $selectedChoices = array($selected); } $multiselectChoices = array(); foreach ($selectedChoices as $value) { $results = array(); foreach ($choices as $key => $choice) { if ($choice === $value) { $results[] = $key; } } if (count($results) > 1) { throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of %s.', implode(' or ', $results))); } $result = array_search($value, $choices); if (!$isAssoc) { if (false !== $result) { $result = $choices[$result]; } elseif (isset($choices[$value])) { $result = $choices[$value]; } } elseif (false === $result && isset($choices[$value])) { $result = $value; } if (false === $result) { throw new InvalidArgumentException(sprintf($errorMessage, $value)); } $multiselectChoices[] = (string) $result; } if ($multiselect) { return $multiselectChoices; } return current($multiselectChoices); }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Question; /** * Represents a yes/no question. * * @author Fabien Potencier */ class ConfirmationQuestion extends Question { private $trueAnswerRegex; /** * Constructor. * * @param string $question The question to ask to the user * @param bool $default The default answer to return, true or false * @param string $trueAnswerRegex A regex to match the "yes" answer */ public function __construct($question, $default = true, $trueAnswerRegex = '/^y/i') { parent::__construct($question, (bool) $default); $this->trueAnswerRegex = $trueAnswerRegex; $this->setNormalizer($this->getDefaultNormalizer()); } /** * Returns the default answer normalizer. * * @return callable */ private function getDefaultNormalizer() { $default = $this->getDefault(); $regex = $this->trueAnswerRegex; return function ($answer) use ($default, $regex) { if (is_bool($answer)) { return $answer; } $answerIsTrue = (bool) preg_match($regex, $answer); if (false === $default) { return $answer && $answerIsTrue; } return !$answer || $answerIsTrue; }; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Question; use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; /** * Represents a Question. * * @author Fabien Potencier */ class Question { private $question; private $attempts; private $hidden = false; private $hiddenFallback = true; private $autocompleterValues; private $validator; private $default; private $normalizer; /** * Constructor. * * @param string $question The question to ask to the user * @param mixed $default The default answer to return if the user enters nothing */ public function __construct($question, $default = null) { $this->question = $question; $this->default = $default; } /** * Returns the question. * * @return string */ public function getQuestion() { return $this->question; } /** * Returns the default answer. * * @return mixed */ public function getDefault() { return $this->default; } /** * Returns whether the user response must be hidden. * * @return bool */ public function isHidden() { return $this->hidden; } /** * Sets whether the user response must be hidden or not. * * @param bool $hidden * * @return Question The current instance * * @throws LogicException In case the autocompleter is also used */ public function setHidden($hidden) { if ($this->autocompleterValues) { throw new LogicException('A hidden question cannot use the autocompleter.'); } $this->hidden = (bool) $hidden; return $this; } /** * In case the response can not be hidden, whether to fallback on non-hidden question or not. * * @return bool */ public function isHiddenFallback() { return $this->hiddenFallback; } /** * Sets whether to fallback on non-hidden question if the response can not be hidden. * * @param bool $fallback * * @return Question The current instance */ public function setHiddenFallback($fallback) { $this->hiddenFallback = (bool) $fallback; return $this; } /** * Gets values for the autocompleter. * * @return null|array|\Traversable */ public function getAutocompleterValues() { return $this->autocompleterValues; } /** * Sets values for the autocompleter. * * @param null|array|\Traversable $values * * @return Question The current instance * * @throws InvalidArgumentException * @throws LogicException */ public function setAutocompleterValues($values) { if (is_array($values)) { $values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values); } if (null !== $values && !is_array($values)) { if (!$values instanceof \Traversable || !$values instanceof \Countable) { throw new InvalidArgumentException('Autocompleter values can be either an array, `null` or an object implementing both `Countable` and `Traversable` interfaces.'); } } if ($this->hidden) { throw new LogicException('A hidden question cannot use the autocompleter.'); } $this->autocompleterValues = $values; return $this; } /** * Sets a validator for the question. * * @param null|callable $validator * * @return Question The current instance */ public function setValidator($validator) { $this->validator = $validator; return $this; } /** * Gets the validator for the question. * * @return null|callable */ public function getValidator() { return $this->validator; } /** * Sets the maximum number of attempts. * * Null means an unlimited number of attempts. * * @param null|int $attempts * * @return Question The current instance * * @throws InvalidArgumentException In case the number of attempts is invalid. */ public function setMaxAttempts($attempts) { if (null !== $attempts && $attempts < 1) { throw new InvalidArgumentException('Maximum number of attempts must be a positive value.'); } $this->attempts = $attempts; return $this; } /** * Gets the maximum number of attempts. * * Null means an unlimited number of attempts. * * @return null|int */ public function getMaxAttempts() { return $this->attempts; } /** * Sets a normalizer for the response. * * The normalizer can be a callable (a string), a closure or a class implementing __invoke. * * @param callable $normalizer * * @return Question The current instance */ public function setNormalizer($normalizer) { $this->normalizer = $normalizer; return $this; } /** * Gets the normalizer for the response. * * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. * * @return callable */ public function getNormalizer() { return $this->normalizer; } protected function isAssoc($array) { return (bool) count(array_filter(array_keys($array), 'is_string')); } } * * 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\Exception\RuntimeException; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\Process\ProcessBuilder; use Symfony\Component\Process\PhpExecutableFinder; /** * A Shell wraps an Application to add shell capabilities to it. * * Support for history and completion only works with a PHP compiled * with readline support (either --with-readline or --with-libedit) * * @deprecated since version 2.8, to be removed in 3.0. * * @author Fabien Potencier * @author Martin Hasoň */ class Shell { private $application; private $history; private $output; private $hasReadline; private $processIsolation = false; /** * Constructor. * * If there is no readline support for the current PHP executable * a \RuntimeException exception is thrown. * * @param Application $application An application instance */ public function __construct(Application $application) { @trigger_error('The '.__CLASS__.' class is deprecated since Symfony 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); $this->hasReadline = function_exists('readline'); $this->application = $application; $this->history = getenv('HOME').'/.history_'.$application->getName(); $this->output = new ConsoleOutput(); } /** * Runs the shell. */ public function run() { $this->application->setAutoExit(false); $this->application->setCatchExceptions(true); if ($this->hasReadline) { readline_read_history($this->history); readline_completion_function(array($this, 'autocompleter')); } $this->output->writeln($this->getHeader()); $php = null; if ($this->processIsolation) { $finder = new PhpExecutableFinder(); $php = $finder->find(); $this->output->writeln(<<<'EOF' Running with process isolation, you should consider this: * each command is executed as separate process, * commands don't support interactivity, all params must be passed explicitly, * commands output is not colorized. EOF ); } while (true) { $command = $this->readline(); if (false === $command) { $this->output->writeln("\n"); break; } if ($this->hasReadline) { readline_add_history($command); readline_write_history($this->history); } if ($this->processIsolation) { $pb = new ProcessBuilder(); $process = $pb ->add($php) ->add($_SERVER['argv'][0]) ->add($command) ->inheritEnvironmentVariables(true) ->getProcess() ; $output = $this->output; $process->run(function ($type, $data) use ($output) { $output->writeln($data); }); $ret = $process->getExitCode(); } else { $ret = $this->application->run(new StringInput($command), $this->output); } if (0 !== $ret) { $this->output->writeln(sprintf('The command terminated with an error status (%s)', $ret)); } } } /** * Returns the shell header. * * @return string The header string */ protected function getHeader() { return <<{$this->application->getName()} shell ({$this->application->getVersion()}). At the prompt, type help for some help, or list to get a list of available commands. To exit the shell, type ^D. EOF; } /** * Renders a prompt. * * @return string The prompt */ protected function getPrompt() { // using the formatter here is required when using readline return $this->output->getFormatter()->format($this->application->getName().' > '); } protected function getOutput() { return $this->output; } protected function getApplication() { return $this->application; } /** * Tries to return autocompletion for the current entered text. * * @param string $text The last segment of the entered text * * @return bool|array A list of guessed strings or true */ private function autocompleter($text) { $info = readline_info(); $text = substr($info['line_buffer'], 0, $info['end']); if ($info['point'] !== $info['end']) { return true; } // task name? if (false === strpos($text, ' ') || !$text) { return array_keys($this->application->all()); } // options and arguments? try { $command = $this->application->find(substr($text, 0, strpos($text, ' '))); } catch (\Exception $e) { return true; } $list = array('--help'); foreach ($command->getDefinition()->getOptions() as $option) { $list[] = '--'.$option->getName(); } return $list; } /** * Reads a single line from standard input. * * @return string The single line from standard input */ private function readline() { if ($this->hasReadline) { $line = readline($this->getPrompt()); } else { $this->output->write($this->getPrompt()); $line = fgets(STDIN, 1024); $line = (false === $line || '' === $line) ? false : rtrim($line); } return $line; } public function getProcessIsolation() { return $this->processIsolation; } public function setProcessIsolation($processIsolation) { $this->processIsolation = (bool) $processIsolation; if ($this->processIsolation && !class_exists('Symfony\\Component\\Process\\Process')) { throw new RuntimeException('Unable to isolate processes as the Symfony Process Component is not installed.'); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Style; use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\OutputInterface; /** * Decorates output to add console style guide helpers. * * @author Kevin Bond */ abstract class OutputStyle implements OutputInterface, StyleInterface { private $output; /** * @param OutputInterface $output */ public function __construct(OutputInterface $output) { $this->output = $output; } /** * {@inheritdoc} */ public function newLine($count = 1) { $this->output->write(str_repeat(PHP_EOL, $count)); } /** * @param int $max * * @return ProgressBar */ public function createProgressBar($max = 0) { return new ProgressBar($this->output, $max); } /** * {@inheritdoc} */ public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) { $this->output->write($messages, $newline, $type); } /** * {@inheritdoc} */ public function writeln($messages, $type = self::OUTPUT_NORMAL) { $this->output->writeln($messages, $type); } /** * {@inheritdoc} */ public function setVerbosity($level) { $this->output->setVerbosity($level); } /** * {@inheritdoc} */ public function getVerbosity() { return $this->output->getVerbosity(); } /** * {@inheritdoc} */ public function setDecorated($decorated) { $this->output->setDecorated($decorated); } /** * {@inheritdoc} */ public function isDecorated() { return $this->output->isDecorated(); } /** * {@inheritdoc} */ public function setFormatter(OutputFormatterInterface $formatter) { $this->output->setFormatter($formatter); } /** * {@inheritdoc} */ public function getFormatter() { return $this->output->getFormatter(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Style; /** * Output style helpers. * * @author Kevin Bond */ interface StyleInterface { /** * Formats a command title. * * @param string $message */ public function title($message); /** * Formats a section title. * * @param string $message */ public function section($message); /** * Formats a list. * * @param array $elements */ public function listing(array $elements); /** * Formats informational text. * * @param string|array $message */ public function text($message); /** * Formats a success result bar. * * @param string|array $message */ public function success($message); /** * Formats an error result bar. * * @param string|array $message */ public function error($message); /** * Formats an warning result bar. * * @param string|array $message */ public function warning($message); /** * Formats a note admonition. * * @param string|array $message */ public function note($message); /** * Formats a caution admonition. * * @param string|array $message */ public function caution($message); /** * Formats a table. * * @param array $headers * @param array $rows */ public function table(array $headers, array $rows); /** * Asks a question. * * @param string $question * @param string|null $default * @param callable|null $validator * * @return string */ public function ask($question, $default = null, $validator = null); /** * Asks a question with the user input hidden. * * @param string $question * @param callable|null $validator * * @return string */ public function askHidden($question, $validator = null); /** * Asks for confirmation. * * @param string $question * @param bool $default * * @return bool */ public function confirm($question, $default = true); /** * Asks a choice question. * * @param string $question * @param array $choices * @param string|int|null $default * * @return string */ public function choice($question, array $choices, $default = null); /** * Add newline(s). * * @param int $count The number of newlines */ public function newLine($count = 1); /** * Starts the progress output. * * @param int $max Maximum steps (0 if unknown) */ public function progressStart($max = 0); /** * Advances the progress output X steps. * * @param int $step Number of steps to advance */ public function progressAdvance($step = 1); /** * Finishes the progress output. */ public function progressFinish(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Console\Style; use Symfony\Component\Console\Application; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Helper\SymfonyQuestionHelper; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; /** * Output decorator helpers for the Symfony Style Guide. * * @author Kevin Bond */ class SymfonyStyle extends OutputStyle { const MAX_LINE_LENGTH = 120; private $input; private $questionHelper; private $progressBar; private $lineLength; private $bufferedOutput; /** * @param InputInterface $input * @param OutputInterface $output */ public function __construct(InputInterface $input, OutputInterface $output) { $this->input = $input; $this->bufferedOutput = new BufferedOutput($output->getVerbosity(), false, clone $output->getFormatter()); // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. $this->lineLength = min($this->getTerminalWidth() - (int) (DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); parent::__construct($output); } /** * Formats a message as a block of text. * * @param string|array $messages The message to write in the block * @param string|null $type The block type (added in [] on first line) * @param string|null $style The style to apply to the whole block * @param string $prefix The prefix for the block * @param bool $padding Whether to add vertical padding */ public function block($messages, $type = null, $style = null, $prefix = ' ', $padding = false) { $messages = is_array($messages) ? array_values($messages) : array($messages); $this->autoPrependBlock(); $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, true)); $this->newLine(); } /** * {@inheritdoc} */ public function title($message) { $this->autoPrependBlock(); $this->writeln(array( sprintf('%s', $message), sprintf('%s', str_repeat('=', Helper::strlenWithoutDecoration($this->getFormatter(), $message))), )); $this->newLine(); } /** * {@inheritdoc} */ public function section($message) { $this->autoPrependBlock(); $this->writeln(array( sprintf('%s', $message), sprintf('%s', str_repeat('-', Helper::strlenWithoutDecoration($this->getFormatter(), $message))), )); $this->newLine(); } /** * {@inheritdoc} */ public function listing(array $elements) { $this->autoPrependText(); $elements = array_map(function ($element) { return sprintf(' * %s', $element); }, $elements); $this->writeln($elements); $this->newLine(); } /** * {@inheritdoc} */ public function text($message) { $this->autoPrependText(); $messages = is_array($message) ? array_values($message) : array($message); foreach ($messages as $message) { $this->writeln(sprintf(' %s', $message)); } } /** * Formats a command comment. * * @param string|array $message */ public function comment($message) { $messages = is_array($message) ? array_values($message) : array($message); $this->autoPrependBlock(); $this->writeln($this->createBlock($messages, null, null, ' // ')); $this->newLine(); } /** * {@inheritdoc} */ public function success($message) { $this->block($message, 'OK', 'fg=black;bg=green', ' ', true); } /** * {@inheritdoc} */ public function error($message) { $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true); } /** * {@inheritdoc} */ public function warning($message) { $this->block($message, 'WARNING', 'fg=white;bg=red', ' ', true); } /** * {@inheritdoc} */ public function note($message) { $this->block($message, 'NOTE', 'fg=yellow', ' ! '); } /** * {@inheritdoc} */ public function caution($message) { $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true); } /** * {@inheritdoc} */ public function table(array $headers, array $rows) { $style = clone Table::getStyleDefinition('symfony-style-guide'); $style->setCellHeaderFormat('%s'); $table = new Table($this); $table->setHeaders($headers); $table->setRows($rows); $table->setStyle($style); $table->render(); $this->newLine(); } /** * {@inheritdoc} */ public function ask($question, $default = null, $validator = null) { $question = new Question($question, $default); $question->setValidator($validator); return $this->askQuestion($question); } /** * {@inheritdoc} */ public function askHidden($question, $validator = null) { $question = new Question($question); $question->setHidden(true); $question->setValidator($validator); return $this->askQuestion($question); } /** * {@inheritdoc} */ public function confirm($question, $default = true) { return $this->askQuestion(new ConfirmationQuestion($question, $default)); } /** * {@inheritdoc} */ public function choice($question, array $choices, $default = null) { if (null !== $default) { $values = array_flip($choices); $default = $values[$default]; } return $this->askQuestion(new ChoiceQuestion($question, $choices, $default)); } /** * {@inheritdoc} */ public function progressStart($max = 0) { $this->progressBar = $this->createProgressBar($max); $this->progressBar->start(); } /** * {@inheritdoc} */ public function progressAdvance($step = 1) { $this->getProgressBar()->advance($step); } /** * {@inheritdoc} */ public function progressFinish() { $this->getProgressBar()->finish(); $this->newLine(2); $this->progressBar = null; } /** * {@inheritdoc} */ public function createProgressBar($max = 0) { $progressBar = parent::createProgressBar($max); if ('\\' !== DIRECTORY_SEPARATOR) { $progressBar->setEmptyBarCharacter('░'); // light shade character \u2591 $progressBar->setProgressCharacter(''); $progressBar->setBarCharacter('▓'); // dark shade character \u2593 } return $progressBar; } /** * @param Question $question * * @return string */ public function askQuestion(Question $question) { if ($this->input->isInteractive()) { $this->autoPrependBlock(); } if (!$this->questionHelper) { $this->questionHelper = new SymfonyQuestionHelper(); } $answer = $this->questionHelper->ask($this->input, $this, $question); if ($this->input->isInteractive()) { $this->newLine(); $this->bufferedOutput->write("\n"); } return $answer; } /** * {@inheritdoc} */ public function writeln($messages, $type = self::OUTPUT_NORMAL) { parent::writeln($messages, $type); $this->bufferedOutput->writeln($this->reduceBuffer($messages), $type); } /** * {@inheritdoc} */ public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) { parent::write($messages, $newline, $type); $this->bufferedOutput->write($this->reduceBuffer($messages), $newline, $type); } /** * {@inheritdoc} */ public function newLine($count = 1) { parent::newLine($count); $this->bufferedOutput->write(str_repeat("\n", $count)); } /** * @return ProgressBar */ private function getProgressBar() { if (!$this->progressBar) { throw new RuntimeException('The ProgressBar is not started.'); } return $this->progressBar; } private function getTerminalWidth() { $application = new Application(); $dimensions = $application->getTerminalDimensions(); return $dimensions[0] ?: self::MAX_LINE_LENGTH; } private function autoPrependBlock() { $chars = substr(str_replace(PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2); if (!isset($chars[0])) { return $this->newLine(); //empty history, so we should start with a new line. } //Prepend new line for each non LF chars (This means no blank line was output before) $this->newLine(2 - substr_count($chars, "\n")); } private function autoPrependText() { $fetched = $this->bufferedOutput->fetch(); //Prepend new line if last char isn't EOL: if ("\n" !== substr($fetched, -1)) { $this->newLine(); } } private function reduceBuffer($messages) { // We need to know if the two last chars are PHP_EOL // Preserve the last 4 chars inserted (PHP_EOL on windows is two chars) in the history buffer return array_map(function ($value) { return substr($value, -4); }, array_merge(array($this->bufferedOutput->fetch()), (array) $messages)); } private function createBlock($messages, $type = null, $style = null, $prefix = ' ', $padding = false, $escape = false) { $indentLength = 0; $prefixLength = Helper::strlenWithoutDecoration($this->getFormatter(), $prefix); $lines = array(); if (null !== $type) { $type = sprintf('[%s] ', $type); $indentLength = strlen($type); $lineIndentation = str_repeat(' ', $indentLength); } // wrap and add newlines for each element foreach ($messages as $key => $message) { if ($escape) { $message = OutputFormatter::escape($message); } $lines = array_merge($lines, explode(PHP_EOL, wordwrap($message, $this->lineLength - $prefixLength - $indentLength, PHP_EOL, true))); if (count($messages) > 1 && $key < count($messages) - 1) { $lines[] = ''; } } $firstLineIndex = 0; if ($padding && $this->isDecorated()) { $firstLineIndex = 1; array_unshift($lines, ''); $lines[] = ''; } foreach ($lines as $i => &$line) { if (null !== $type) { $line = $firstLineIndex === $i ? $type.$line : $lineIndentation.$line; } $line = $prefix.$line; $line .= str_repeat(' ', $this->lineLength - Helper::strlenWithoutDecoration($this->getFormatter(), $line)); if ($style) { $line = sprintf('<%s>%s', $style, $line); } } return $lines; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug; use Psr\Log\AbstractLogger; /** * A buffering logger that stacks logs for later. * * @author Nicolas Grekas */ class BufferingLogger extends AbstractLogger { private $logs = array(); public function log($level, $message, array $context = array()) { $this->logs[] = array($level, $message, $context); } public function cleanLogs() { $logs = $this->logs; $this->logs = array(); return $logs; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug; /** * Registers all the debug tools. * * @author Fabien Potencier */ class Debug { private static $enabled = false; /** * Enables the debug tools. * * This method registers an error handler and an exception handler. * * If the Symfony ClassLoader component is available, a special * class loader is also registered. * * @param int $errorReportingLevel The level of error reporting you want * @param bool $displayErrors Whether to display errors (for development) or just log them (for production) */ public static function enable($errorReportingLevel = null, $displayErrors = true) { if (static::$enabled) { return; } static::$enabled = true; if (null !== $errorReportingLevel) { error_reporting($errorReportingLevel); } else { error_reporting(-1); } if ('cli' !== PHP_SAPI) { ini_set('display_errors', 0); ExceptionHandler::register(); } elseif ($displayErrors && (!ini_get('log_errors') || ini_get('error_log'))) { // CLI - display errors only if they're not already logged to STDERR ini_set('display_errors', 1); } if ($displayErrors) { ErrorHandler::register(new ErrorHandler(new BufferingLogger())); } else { ErrorHandler::register()->throwAt(0, true); } DebugClassLoader::enable(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug; /** * Autoloader checking if the class is really defined in the file found. * * The ClassLoader will wrap all registered autoloaders * and will throw an exception if a file is found but does * not declare the class. * * @author Fabien Potencier * @author Christophe Coevoet * @author Nicolas Grekas */ class DebugClassLoader { private $classLoader; private $isFinder; private $wasFinder; private static $caseCheck; private static $deprecated = array(); private static $php7Reserved = array('int', 'float', 'bool', 'string', 'true', 'false', 'null'); private static $darwinCache = array('/' => array('/', array())); /** * Constructor. * * @param callable|object $classLoader Passing an object is @deprecated since version 2.5 and support for it will be removed in 3.0 */ public function __construct($classLoader) { $this->wasFinder = is_object($classLoader) && method_exists($classLoader, 'findFile'); if ($this->wasFinder) { @trigger_error('The '.__METHOD__.' method will no longer support receiving an object into its $classLoader argument in 3.0.', E_USER_DEPRECATED); $this->classLoader = array($classLoader, 'loadClass'); $this->isFinder = true; } else { $this->classLoader = $classLoader; $this->isFinder = is_array($classLoader) && method_exists($classLoader[0], 'findFile'); } if (!isset(self::$caseCheck)) { $file = file_exists(__FILE__) ? __FILE__ : rtrim(realpath('.'), DIRECTORY_SEPARATOR); $i = strrpos($file, DIRECTORY_SEPARATOR); $dir = substr($file, 0, 1 + $i); $file = substr($file, 1 + $i); $test = strtoupper($file) === $file ? strtolower($file) : strtoupper($file); $test = realpath($dir.$test); if (false === $test || false === $i) { // filesystem is case sensitive self::$caseCheck = 0; } elseif (substr($test, -strlen($file)) === $file) { // filesystem is case insensitive and realpath() normalizes the case of characters self::$caseCheck = 1; } elseif (false !== stripos(PHP_OS, 'darwin')) { // on MacOSX, HFS+ is case insensitive but realpath() doesn't normalize the case of characters self::$caseCheck = 2; } else { // filesystem case checks failed, fallback to disabling them self::$caseCheck = 0; } } } /** * Gets the wrapped class loader. * * @return callable|object A class loader. Since version 2.5, returning an object is @deprecated and support for it will be removed in 3.0 */ public function getClassLoader() { return $this->wasFinder ? $this->classLoader[0] : $this->classLoader; } /** * Wraps all autoloaders. */ public static function enable() { // Ensures we don't hit https://bugs.php.net/42098 class_exists('Symfony\Component\Debug\ErrorHandler'); class_exists('Psr\Log\LogLevel'); if (!is_array($functions = spl_autoload_functions())) { return; } foreach ($functions as $function) { spl_autoload_unregister($function); } foreach ($functions as $function) { if (!is_array($function) || !$function[0] instanceof self) { $function = array(new static($function), 'loadClass'); } spl_autoload_register($function); } } /** * Disables the wrapping. */ public static function disable() { if (!is_array($functions = spl_autoload_functions())) { return; } foreach ($functions as $function) { spl_autoload_unregister($function); } foreach ($functions as $function) { if (is_array($function) && $function[0] instanceof self) { $function = $function[0]->getClassLoader(); } spl_autoload_register($function); } } /** * Finds a file by class name. * * @param string $class A class name to resolve to file * * @return string|null * * @deprecated since version 2.5, to be removed in 3.0. */ public function findFile($class) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.5 and will be removed in 3.0.', E_USER_DEPRECATED); if ($this->wasFinder) { return $this->classLoader[0]->findFile($class); } } /** * Loads the given class or interface. * * @param string $class The name of the class * * @return bool|null True, if loaded * * @throws \RuntimeException */ public function loadClass($class) { ErrorHandler::stackErrors(); try { if ($this->isFinder) { if ($file = $this->classLoader[0]->findFile($class)) { require_once $file; } } else { call_user_func($this->classLoader, $class); $file = false; } } catch (\Exception $e) { ErrorHandler::unstackErrors(); throw $e; } catch (\Throwable $e) { ErrorHandler::unstackErrors(); throw $e; } ErrorHandler::unstackErrors(); $exists = class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false)); if ('\\' === $class[0]) { $class = substr($class, 1); } if ($exists) { $refl = new \ReflectionClass($class); $name = $refl->getName(); if ($name !== $class && 0 === strcasecmp($name, $class)) { throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: %s vs %s', $class, $name)); } if (in_array(strtolower($refl->getShortName()), self::$php7Reserved)) { @trigger_error(sprintf('%s uses a reserved class name (%s) that will break on PHP 7 and higher', $name, $refl->getShortName()), E_USER_DEPRECATED); } elseif (preg_match('#\n \* @deprecated (.*?)\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) { self::$deprecated[$name] = preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]); } else { if (2 > $len = 1 + (strpos($name, '\\', 1 + strpos($name, '\\')) ?: strpos($name, '_'))) { $len = 0; $ns = ''; } else { switch ($ns = substr($name, 0, $len)) { case 'Symfony\Bridge\\': case 'Symfony\Bundle\\': case 'Symfony\Component\\': $ns = 'Symfony\\'; $len = strlen($ns); break; } } $parent = get_parent_class($class); if (!$parent || strncmp($ns, $parent, $len)) { if ($parent && isset(self::$deprecated[$parent]) && strncmp($ns, $parent, $len)) { @trigger_error(sprintf('The %s class extends %s that is deprecated %s', $name, $parent, self::$deprecated[$parent]), E_USER_DEPRECATED); } $parentInterfaces = array(); $deprecatedInterfaces = array(); if ($parent) { foreach (class_implements($parent) as $interface) { $parentInterfaces[$interface] = 1; } } foreach ($refl->getInterfaceNames() as $interface) { if (isset(self::$deprecated[$interface]) && strncmp($ns, $interface, $len)) { $deprecatedInterfaces[] = $interface; } foreach (class_implements($interface) as $interface) { $parentInterfaces[$interface] = 1; } } foreach ($deprecatedInterfaces as $interface) { if (!isset($parentInterfaces[$interface])) { @trigger_error(sprintf('The %s %s %s that is deprecated %s', $name, $refl->isInterface() ? 'interface extends' : 'class implements', $interface, self::$deprecated[$interface]), E_USER_DEPRECATED); } } } } } if ($file) { if (!$exists) { if (false !== strpos($class, '/')) { throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class)); } throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); } if (self::$caseCheck) { $real = explode('\\', $class.strrchr($file, '.')); $tail = explode(DIRECTORY_SEPARATOR, str_replace('/', DIRECTORY_SEPARATOR, $file)); $i = count($tail) - 1; $j = count($real) - 1; while (isset($tail[$i], $real[$j]) && $tail[$i] === $real[$j]) { --$i; --$j; } array_splice($tail, 0, $i + 1); } if (self::$caseCheck && $tail) { $tail = DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $tail); $tailLen = strlen($tail); $real = $refl->getFileName(); if (2 === self::$caseCheck) { // realpath() on MacOSX doesn't normalize the case of characters $i = 1 + strrpos($real, '/'); $file = substr($real, $i); $real = substr($real, 0, $i); if (isset(self::$darwinCache[$real])) { $kDir = $real; } else { $kDir = strtolower($real); if (isset(self::$darwinCache[$kDir])) { $real = self::$darwinCache[$kDir][0]; } else { $dir = getcwd(); chdir($real); $real = getcwd().'/'; chdir($dir); $dir = $real; $k = $kDir; $i = strlen($dir) - 1; while (!isset(self::$darwinCache[$k])) { self::$darwinCache[$k] = array($dir, array()); self::$darwinCache[$dir] = &self::$darwinCache[$k]; while ('/' !== $dir[--$i]) { } $k = substr($k, 0, ++$i); $dir = substr($dir, 0, $i--); } } } $dirFiles = self::$darwinCache[$kDir][1]; if (isset($dirFiles[$file])) { $kFile = $file; } else { $kFile = strtolower($file); if (!isset($dirFiles[$kFile])) { foreach (scandir($real, 2) as $f) { if ('.' !== $f[0]) { $dirFiles[$f] = $f; if ($f === $file) { $kFile = $k = $file; } elseif ($f !== $k = strtolower($f)) { $dirFiles[$k] = $f; } } } self::$darwinCache[$kDir][1] = $dirFiles; } } $real .= $dirFiles[$kFile]; } if (0 === substr_compare($real, $tail, -$tailLen, $tailLen, true) && 0 !== substr_compare($real, $tail, -$tailLen, $tailLen, false) ) { throw new \RuntimeException(sprintf('Case mismatch between class and real file names: %s vs %s in %s', substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1))); } } return true; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug; use Psr\Log\LogLevel; use Psr\Log\LoggerInterface; use Symfony\Component\Debug\Exception\ContextErrorException; use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\Debug\Exception\OutOfMemoryException; use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler; use Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface; /** * A generic ErrorHandler for the PHP engine. * * Provides five bit fields that control how errors are handled: * - thrownErrors: errors thrown as \ErrorException * - loggedErrors: logged errors, when not @-silenced * - scopedErrors: errors thrown or logged with their local context * - tracedErrors: errors logged with their stack trace, only once for repeated errors * - screamedErrors: never @-silenced errors * * Each error level can be logged by a dedicated PSR-3 logger object. * Screaming only applies to logging. * Throwing takes precedence over logging. * Uncaught exceptions are logged as E_ERROR. * E_DEPRECATED and E_USER_DEPRECATED levels never throw. * E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw. * Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so. * As errors have a performance cost, repeated errors are all logged, so that the developer * can see them and weight them as more important to fix than others of the same level. * * @author Nicolas Grekas */ class ErrorHandler { /** * @deprecated since version 2.6, to be removed in 3.0. */ const TYPE_DEPRECATION = -100; private $levels = array( E_DEPRECATED => 'Deprecated', E_USER_DEPRECATED => 'User Deprecated', E_NOTICE => 'Notice', E_USER_NOTICE => 'User Notice', E_STRICT => 'Runtime Notice', E_WARNING => 'Warning', E_USER_WARNING => 'User Warning', E_COMPILE_WARNING => 'Compile Warning', E_CORE_WARNING => 'Core Warning', E_USER_ERROR => 'User Error', E_RECOVERABLE_ERROR => 'Catchable Fatal Error', E_COMPILE_ERROR => 'Compile Error', E_PARSE => 'Parse Error', E_ERROR => 'Error', E_CORE_ERROR => 'Core Error', ); private $loggers = array( E_DEPRECATED => array(null, LogLevel::INFO), E_USER_DEPRECATED => array(null, LogLevel::INFO), E_NOTICE => array(null, LogLevel::WARNING), E_USER_NOTICE => array(null, LogLevel::WARNING), E_STRICT => array(null, LogLevel::WARNING), E_WARNING => array(null, LogLevel::WARNING), E_USER_WARNING => array(null, LogLevel::WARNING), E_COMPILE_WARNING => array(null, LogLevel::WARNING), E_CORE_WARNING => array(null, LogLevel::WARNING), E_USER_ERROR => array(null, LogLevel::CRITICAL), E_RECOVERABLE_ERROR => array(null, LogLevel::CRITICAL), E_COMPILE_ERROR => array(null, LogLevel::CRITICAL), E_PARSE => array(null, LogLevel::CRITICAL), E_ERROR => array(null, LogLevel::CRITICAL), E_CORE_ERROR => array(null, LogLevel::CRITICAL), ); private $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED private $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED private $tracedErrors = 0x77FB; // E_ALL - E_STRICT - E_PARSE private $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE private $loggedErrors = 0; private $loggedTraces = array(); private $isRecursive = 0; private $isRoot = false; private $exceptionHandler; private $bootstrappingLogger; private static $reservedMemory; private static $stackedErrors = array(); private static $stackedErrorLevels = array(); private static $toStringException = null; /** * Same init value as thrownErrors. * * @deprecated since version 2.6, to be removed in 3.0. */ private $displayErrors = 0x1FFF; /** * Registers the error handler. * * @param self|null|int $handler The handler to register, or @deprecated (since version 2.6, to be removed in 3.0) bit field of thrown levels * @param bool $replace Whether to replace or not any existing handler * * @return self The registered error handler */ public static function register($handler = null, $replace = true) { if (null === self::$reservedMemory) { self::$reservedMemory = str_repeat('x', 10240); register_shutdown_function(__CLASS__.'::handleFatalError'); } $levels = -1; if ($handlerIsNew = !$handler instanceof self) { // @deprecated polymorphism, to be removed in 3.0 if (null !== $handler) { $levels = $replace ? $handler : 0; $replace = true; } $handler = new static(); } if (null === $prev = set_error_handler(array($handler, 'handleError'))) { restore_error_handler(); // Specifying the error types earlier would expose us to https://bugs.php.net/63206 set_error_handler(array($handler, 'handleError'), $handler->thrownErrors | $handler->loggedErrors); $handler->isRoot = true; } if ($handlerIsNew && is_array($prev) && $prev[0] instanceof self) { $handler = $prev[0]; $replace = false; } if ($replace || !$prev) { $handler->setExceptionHandler(set_exception_handler(array($handler, 'handleException'))); } else { restore_error_handler(); } $handler->throwAt($levels & $handler->thrownErrors, true); return $handler; } public function __construct(BufferingLogger $bootstrappingLogger = null) { if ($bootstrappingLogger) { $this->bootstrappingLogger = $bootstrappingLogger; $this->setDefaultLogger($bootstrappingLogger); } } /** * Sets a logger to non assigned errors levels. * * @param LoggerInterface $logger A PSR-3 logger to put as default for the given levels * @param array|int $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants * @param bool $replace Whether to replace or not any existing logger */ public function setDefaultLogger(LoggerInterface $logger, $levels = null, $replace = false) { $loggers = array(); if (is_array($levels)) { foreach ($levels as $type => $logLevel) { if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) { $loggers[$type] = array($logger, $logLevel); } } } else { if (null === $levels) { $levels = E_ALL | E_STRICT; } foreach ($this->loggers as $type => $log) { if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) { $log[0] = $logger; $loggers[$type] = $log; } } } $this->setLoggers($loggers); } /** * Sets a logger for each error level. * * @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map * * @return array The previous map * * @throws \InvalidArgumentException */ public function setLoggers(array $loggers) { $prevLogged = $this->loggedErrors; $prev = $this->loggers; $flush = array(); foreach ($loggers as $type => $log) { if (!isset($prev[$type])) { throw new \InvalidArgumentException('Unknown error type: '.$type); } if (!is_array($log)) { $log = array($log); } elseif (!array_key_exists(0, $log)) { throw new \InvalidArgumentException('No logger provided'); } if (null === $log[0]) { $this->loggedErrors &= ~$type; } elseif ($log[0] instanceof LoggerInterface) { $this->loggedErrors |= $type; } else { throw new \InvalidArgumentException('Invalid logger provided'); } $this->loggers[$type] = $log + $prev[$type]; if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) { $flush[$type] = $type; } } $this->reRegister($prevLogged | $this->thrownErrors); if ($flush) { foreach ($this->bootstrappingLogger->cleanLogs() as $log) { $type = $log[2]['type']; if (!isset($flush[$type])) { $this->bootstrappingLogger->log($log[0], $log[1], $log[2]); } elseif ($this->loggers[$type][0]) { $this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]); } } } return $prev; } /** * Sets a user exception handler. * * @param callable $handler A handler that will be called on Exception * * @return callable|null The previous exception handler * * @throws \InvalidArgumentException */ public function setExceptionHandler($handler) { if (null !== $handler && !is_callable($handler)) { throw new \LogicException('The exception handler must be a valid PHP callable.'); } $prev = $this->exceptionHandler; $this->exceptionHandler = $handler; return $prev; } /** * Sets the PHP error levels that throw an exception when a PHP error occurs. * * @param int $levels A bit field of E_* constants for thrown errors * @param bool $replace Replace or amend the previous value * * @return int The previous value */ public function throwAt($levels, $replace = false) { $prev = $this->thrownErrors; $this->thrownErrors = ($levels | E_RECOVERABLE_ERROR | E_USER_ERROR) & ~E_USER_DEPRECATED & ~E_DEPRECATED; if (!$replace) { $this->thrownErrors |= $prev; } $this->reRegister($prev | $this->loggedErrors); // $this->displayErrors is @deprecated since version 2.6 $this->displayErrors = $this->thrownErrors; return $prev; } /** * Sets the PHP error levels for which local variables are preserved. * * @param int $levels A bit field of E_* constants for scoped errors * @param bool $replace Replace or amend the previous value * * @return int The previous value */ public function scopeAt($levels, $replace = false) { $prev = $this->scopedErrors; $this->scopedErrors = (int) $levels; if (!$replace) { $this->scopedErrors |= $prev; } return $prev; } /** * Sets the PHP error levels for which the stack trace is preserved. * * @param int $levels A bit field of E_* constants for traced errors * @param bool $replace Replace or amend the previous value * * @return int The previous value */ public function traceAt($levels, $replace = false) { $prev = $this->tracedErrors; $this->tracedErrors = (int) $levels; if (!$replace) { $this->tracedErrors |= $prev; } return $prev; } /** * Sets the error levels where the @-operator is ignored. * * @param int $levels A bit field of E_* constants for screamed errors * @param bool $replace Replace or amend the previous value * * @return int The previous value */ public function screamAt($levels, $replace = false) { $prev = $this->screamedErrors; $this->screamedErrors = (int) $levels; if (!$replace) { $this->screamedErrors |= $prev; } return $prev; } /** * Re-registers as a PHP error handler if levels changed. */ private function reRegister($prev) { if ($prev !== $this->thrownErrors | $this->loggedErrors) { $handler = set_error_handler('var_dump'); $handler = is_array($handler) ? $handler[0] : null; restore_error_handler(); if ($handler === $this) { restore_error_handler(); if ($this->isRoot) { set_error_handler(array($this, 'handleError'), $this->thrownErrors | $this->loggedErrors); } else { set_error_handler(array($this, 'handleError')); } } } } /** * Handles errors by filtering then logging them according to the configured bit fields. * * @param int $type One of the E_* constants * @param string $message * @param string $file * @param int $line * @param array $context * @param array $backtrace * * @return bool Returns false when no handling happens so that the PHP engine can handle the error itself * * @throws \ErrorException When $this->thrownErrors requests so * * @internal */ public function handleError($type, $message, $file, $line, array $context, array $backtrace = null) { $level = error_reporting() | E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED; $log = $this->loggedErrors & $type; $throw = $this->thrownErrors & $type & $level; $type &= $level | $this->screamedErrors; if (!$type || (!$log && !$throw)) { return $type && $log; } if (isset($context['GLOBALS']) && ($this->scopedErrors & $type)) { $e = $context; // Whatever the signature of the method, unset($e['GLOBALS'], $context); // $context is always a reference in 5.3 $context = $e; } if (null !== $backtrace && $type & E_ERROR) { // E_ERROR fatal errors are triggered on HHVM when // hhvm.error_handling.call_user_handler_on_fatals=1 // which is the way to get their backtrace. $this->handleFatalError(compact('type', 'message', 'file', 'line', 'backtrace')); return true; } if ($throw) { if (null !== self::$toStringException) { $throw = self::$toStringException; self::$toStringException = null; } elseif (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) { // Checking for class existence is a work around for https://bugs.php.net/42098 $throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context); } else { $throw = new \ErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line); } if (PHP_VERSION_ID <= 50407 && (PHP_VERSION_ID >= 50400 || PHP_VERSION_ID <= 50317)) { // Exceptions thrown from error handlers are sometimes not caught by the exception // handler and shutdown handlers are bypassed before 5.4.8/5.3.18. // We temporarily re-enable display_errors to prevent any blank page related to this bug. $throw->errorHandlerCanary = new ErrorHandlerCanary(); } if (E_USER_ERROR & $type) { $backtrace = $backtrace ?: $throw->getTrace(); for ($i = 1; isset($backtrace[$i]); ++$i) { if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function']) && '__toString' === $backtrace[$i]['function'] && '->' === $backtrace[$i]['type'] && !isset($backtrace[$i - 1]['class']) && ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function']) ) { // Here, we know trigger_error() has been called from __toString(). // HHVM is fine with throwing from __toString() but PHP triggers a fatal error instead. // A small convention allows working around the limitation: // given a caught $e exception in __toString(), quitting the method with // `return trigger_error($e, E_USER_ERROR);` allows this error handler // to make $e get through the __toString() barrier. foreach ($context as $e) { if (($e instanceof \Exception || $e instanceof \Throwable) && $e->__toString() === $message) { if (1 === $i) { // On HHVM $throw = $e; break; } self::$toStringException = $e; return true; } } if (1 < $i) { // On PHP (not on HHVM), display the original error message instead of the default one. $this->handleException($throw); // Stop the process by giving back the error to the native handler. return false; } } } } throw $throw; } // For duplicated errors, log the trace only once $e = md5("{$type}/{$line}/{$file}\x00{$message}", true); $trace = true; if (!($this->tracedErrors & $type) || isset($this->loggedTraces[$e])) { $trace = false; } else { $this->loggedTraces[$e] = 1; } $e = compact('type', 'file', 'line', 'level'); if ($type & $level) { if ($this->scopedErrors & $type) { $e['scope_vars'] = $context; if ($trace) { $e['stack'] = $backtrace ?: debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT); } } elseif ($trace) { if (null === $backtrace) { $e['stack'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); } else { foreach ($backtrace as &$frame) { unset($frame['args'], $frame); } $e['stack'] = $backtrace; } } } if ($this->isRecursive) { $log = 0; } elseif (self::$stackedErrorLevels) { self::$stackedErrors[] = array($this->loggers[$type][0], ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG, $message, $e); } else { try { $this->isRecursive = true; $this->loggers[$type][0]->log(($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG, $message, $e); $this->isRecursive = false; } catch (\Exception $e) { $this->isRecursive = false; throw $e; } catch (\Throwable $e) { $this->isRecursive = false; throw $e; } } return $type && $log; } /** * Handles an exception by logging then forwarding it to another handler. * * @param \Exception|\Throwable $exception An exception to handle * @param array $error An array as returned by error_get_last() * * @internal */ public function handleException($exception, array $error = null) { if (!$exception instanceof \Exception) { $exception = new FatalThrowableError($exception); } $type = $exception instanceof FatalErrorException ? $exception->getSeverity() : E_ERROR; if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) { $e = array( 'type' => $type, 'file' => $exception->getFile(), 'line' => $exception->getLine(), 'level' => error_reporting(), 'stack' => $exception->getTrace(), ); if ($exception instanceof FatalErrorException) { if ($exception instanceof FatalThrowableError) { $error = array( 'type' => $type, 'message' => $message = $exception->getMessage(), 'file' => $e['file'], 'line' => $e['line'], ); } else { $message = 'Fatal '.$exception->getMessage(); } } elseif ($exception instanceof \ErrorException) { $message = 'Uncaught '.$exception->getMessage(); if ($exception instanceof ContextErrorException) { $e['context'] = $exception->getContext(); } } else { $message = 'Uncaught Exception: '.$exception->getMessage(); } } if ($this->loggedErrors & $type) { $this->loggers[$type][0]->log($this->loggers[$type][1], $message, $e); } if ($exception instanceof FatalErrorException && !$exception instanceof OutOfMemoryException && $error) { foreach ($this->getFatalErrorHandlers() as $handler) { if ($e = $handler->handleError($error, $exception)) { $exception = $e; break; } } } if (empty($this->exceptionHandler)) { throw $exception; // Give back $exception to the native handler } try { call_user_func($this->exceptionHandler, $exception); } catch (\Exception $handlerException) { } catch (\Throwable $handlerException) { } if (isset($handlerException)) { $this->exceptionHandler = null; $this->handleException($handlerException); } } /** * Shutdown registered function for handling PHP fatal errors. * * @param array $error An array as returned by error_get_last() * * @internal */ public static function handleFatalError(array $error = null) { if (null === self::$reservedMemory) { return; } self::$reservedMemory = null; $handler = set_error_handler('var_dump'); $handler = is_array($handler) ? $handler[0] : null; restore_error_handler(); if (!$handler instanceof self) { return; } if (null === $error) { $error = error_get_last(); } try { while (self::$stackedErrorLevels) { static::unstackErrors(); } } catch (\Exception $exception) { // Handled below } catch (\Throwable $exception) { // Handled below } if ($error && $error['type'] &= E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR) { // Let's not throw anymore but keep logging $handler->throwAt(0, true); $trace = isset($error['backtrace']) ? $error['backtrace'] : null; if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) { $exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false, $trace); } else { $exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace); } } elseif (!isset($exception)) { return; } try { $handler->handleException($exception, $error); } catch (FatalErrorException $e) { // Ignore this re-throw } } /** * Configures the error handler for delayed handling. * Ensures also that non-catchable fatal errors are never silenced. * * As shown by http://bugs.php.net/42098 and http://bugs.php.net/60724 * PHP has a compile stage where it behaves unusually. To workaround it, * we plug an error handler that only stacks errors for later. * * The most important feature of this is to prevent * autoloading until unstackErrors() is called. */ public static function stackErrors() { self::$stackedErrorLevels[] = error_reporting(error_reporting() | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR); } /** * Unstacks stacked errors and forwards to the logger. */ public static function unstackErrors() { $level = array_pop(self::$stackedErrorLevels); if (null !== $level) { $e = error_reporting($level); if ($e !== ($level | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR)) { // If the user changed the error level, do not overwrite it error_reporting($e); } } if (empty(self::$stackedErrorLevels)) { $errors = self::$stackedErrors; self::$stackedErrors = array(); foreach ($errors as $e) { $e[0]->log($e[1], $e[2], $e[3]); } } } /** * Gets the fatal error handlers. * * Override this method if you want to define more fatal error handlers. * * @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface */ protected function getFatalErrorHandlers() { return array( new UndefinedFunctionFatalErrorHandler(), new UndefinedMethodFatalErrorHandler(), new ClassNotFoundFatalErrorHandler(), ); } /** * Sets the level at which the conversion to Exception is done. * * @param int|null $level The level (null to use the error_reporting() value and 0 to disable) * * @deprecated since version 2.6, to be removed in 3.0. Use throwAt() instead. */ public function setLevel($level) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the throwAt() method instead.', E_USER_DEPRECATED); $level = null === $level ? error_reporting() : $level; $this->throwAt($level, true); } /** * Sets the display_errors flag value. * * @param int $displayErrors The display_errors flag value * * @deprecated since version 2.6, to be removed in 3.0. Use throwAt() instead. */ public function setDisplayErrors($displayErrors) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the throwAt() method instead.', E_USER_DEPRECATED); if ($displayErrors) { $this->throwAt($this->displayErrors, true); } else { $displayErrors = $this->displayErrors; $this->throwAt(0, true); $this->displayErrors = $displayErrors; } } /** * Sets a logger for the given channel. * * @param LoggerInterface $logger A logger interface * @param string $channel The channel associated with the logger (deprecation, emergency or scream) * * @deprecated since version 2.6, to be removed in 3.0. Use setLoggers() or setDefaultLogger() instead. */ public static function setLogger(LoggerInterface $logger, $channel = 'deprecation') { @trigger_error('The '.__METHOD__.' static method is deprecated since version 2.6 and will be removed in 3.0. Use the setLoggers() or setDefaultLogger() methods instead.', E_USER_DEPRECATED); $handler = set_error_handler('var_dump'); $handler = is_array($handler) ? $handler[0] : null; restore_error_handler(); if (!$handler instanceof self) { return; } if ('deprecation' === $channel) { $handler->setDefaultLogger($logger, E_DEPRECATED | E_USER_DEPRECATED, true); $handler->screamAt(E_DEPRECATED | E_USER_DEPRECATED); } elseif ('scream' === $channel) { $handler->setDefaultLogger($logger, E_ALL | E_STRICT, false); $handler->screamAt(E_ALL | E_STRICT); } elseif ('emergency' === $channel) { $handler->setDefaultLogger($logger, E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR, true); $handler->screamAt(E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR); } } /** * @deprecated since version 2.6, to be removed in 3.0. Use handleError() instead. */ public function handle($level, $message, $file = 'unknown', $line = 0, $context = array()) { $this->handleError(E_USER_DEPRECATED, 'The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the handleError() method instead.', __FILE__, __LINE__, array()); return $this->handleError($level, $message, $file, $line, (array) $context); } /** * Handles PHP fatal errors. * * @deprecated since version 2.6, to be removed in 3.0. Use handleFatalError() instead. */ public function handleFatal() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the handleFatalError() method instead.', E_USER_DEPRECATED); static::handleFatalError(); } } /** * Private class used to work around https://bugs.php.net/54275. * * @author Nicolas Grekas * * @internal */ class ErrorHandlerCanary { private static $displayErrors = null; public function __construct() { if (null === self::$displayErrors) { self::$displayErrors = ini_set('display_errors', 1); } } public function __destruct() { if (null !== self::$displayErrors) { ini_set('display_errors', self::$displayErrors); self::$displayErrors = null; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\Exception; /** * Class (or Trait or Interface) Not Found Exception. * * @author Konstanton Myakshin */ class ClassNotFoundException extends FatalErrorException { public function __construct($message, \ErrorException $previous) { parent::__construct( $message, $previous->getCode(), $previous->getSeverity(), $previous->getFile(), $previous->getLine(), $previous->getPrevious() ); $this->setTrace($previous->getTrace()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\Exception; /** * Error Exception with Variable Context. * * @author Christian Sciberras */ class ContextErrorException extends \ErrorException { private $context = array(); public function __construct($message, $code, $severity, $filename, $lineno, $context = array()) { parent::__construct($message, $code, $severity, $filename, $lineno); $this->context = $context; } /** * @return array Array of variables that existed when the exception occurred */ public function getContext() { return $this->context; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\Exception; @trigger_error('The '.__NAMESPACE__.'\DummyException class is deprecated since version 2.5 and will be removed in 3.0.', E_USER_DEPRECATED); /** * @author Fabien Potencier * * @deprecated since version 2.5, to be removed in 3.0. */ class DummyException extends \ErrorException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; /** * Fatal Error Exception. * * @author Fabien Potencier * @author Konstanton Myakshin * @author Nicolas Grekas * * @deprecated Deprecated in 2.3, to be removed in 3.0. Use the same class from the Debug component instead. */ class FatalErrorException extends \ErrorException { } namespace Symfony\Component\Debug\Exception; use Symfony\Component\HttpKernel\Exception\FatalErrorException as LegacyFatalErrorException; /** * Fatal Error Exception. * * @author Konstanton Myakshin */ class FatalErrorException extends LegacyFatalErrorException { public function __construct($message, $code, $severity, $filename, $lineno, $traceOffset = null, $traceArgs = true, array $trace = null) { parent::__construct($message, $code, $severity, $filename, $lineno); if (null !== $trace) { if (!$traceArgs) { foreach ($trace as &$frame) { unset($frame['args'], $frame['this'], $frame); } } $this->setTrace($trace); } elseif (null !== $traceOffset) { if (function_exists('xdebug_get_function_stack')) { $trace = xdebug_get_function_stack(); if (0 < $traceOffset) { array_splice($trace, -$traceOffset); } foreach ($trace as &$frame) { if (!isset($frame['type'])) { // XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695 if (isset($frame['class'])) { $frame['type'] = '::'; } } elseif ('dynamic' === $frame['type']) { $frame['type'] = '->'; } elseif ('static' === $frame['type']) { $frame['type'] = '::'; } // XDebug also has a different name for the parameters array if (!$traceArgs) { unset($frame['params'], $frame['args']); } elseif (isset($frame['params']) && !isset($frame['args'])) { $frame['args'] = $frame['params']; unset($frame['params']); } } unset($frame); $trace = array_reverse($trace); } elseif (function_exists('symfony_debug_backtrace')) { $trace = symfony_debug_backtrace(); if (0 < $traceOffset) { array_splice($trace, 0, $traceOffset); } } else { $trace = array(); } $this->setTrace($trace); } } protected function setTrace($trace) { $traceReflector = new \ReflectionProperty('Exception', 'trace'); $traceReflector->setAccessible(true); $traceReflector->setValue($this, $trace); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\Exception; /** * Fatal Throwable Error. * * @author Nicolas Grekas */ class FatalThrowableError extends FatalErrorException { public function __construct(\Throwable $e) { if ($e instanceof \ParseError) { $message = 'Parse error: '.$e->getMessage(); $severity = E_PARSE; } elseif ($e instanceof \TypeError) { $message = 'Type error: '.$e->getMessage(); $severity = E_RECOVERABLE_ERROR; } else { $message = $e->getMessage(); $severity = E_ERROR; } \ErrorException::__construct( $message, $e->getCode(), $severity, $e->getFile(), $e->getLine() ); $this->setTrace($e->getTrace()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\HttpKernel\Exception; use Symfony\Component\Debug\Exception\FlattenException as DebugFlattenException; /** * FlattenException wraps a PHP Exception to be able to serialize it. * * Basically, this class removes all objects from the trace. * * @author Fabien Potencier * * @deprecated Deprecated in 2.3, to be removed in 3.0. Use the same class from the Debug component instead. */ class FlattenException { private $handler; public static function __callStatic($method, $args) { if (!method_exists('Symfony\Component\Debug\Exception\FlattenException', $method)) { throw new \BadMethodCallException(sprintf('Call to undefined method %s::%s()', get_called_class(), $method)); } return call_user_func_array(array('Symfony\Component\Debug\Exception\FlattenException', $method), $args); } public function __call($method, $args) { if (!isset($this->handler)) { $this->handler = new DebugFlattenException(); } if (!method_exists($this->handler, $method)) { throw new \BadMethodCallException(sprintf('Call to undefined method %s::%s()', get_class($this), $method)); } return call_user_func_array(array($this->handler, $method), $args); } } namespace Symfony\Component\Debug\Exception; use Symfony\Component\HttpKernel\Exception\FlattenException as LegacyFlattenException; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; /** * FlattenException wraps a PHP Exception to be able to serialize it. * * Basically, this class removes all objects from the trace. * * @author Fabien Potencier */ class FlattenException extends LegacyFlattenException { private $message; private $code; private $previous; private $trace; private $class; private $statusCode; private $headers; private $file; private $line; public static function create(\Exception $exception, $statusCode = null, array $headers = array()) { $e = new static(); $e->setMessage($exception->getMessage()); $e->setCode($exception->getCode()); if ($exception instanceof HttpExceptionInterface) { $statusCode = $exception->getStatusCode(); $headers = array_merge($headers, $exception->getHeaders()); } if (null === $statusCode) { $statusCode = 500; } $e->setStatusCode($statusCode); $e->setHeaders($headers); $e->setTraceFromException($exception); $e->setClass(get_class($exception)); $e->setFile($exception->getFile()); $e->setLine($exception->getLine()); $previous = $exception->getPrevious(); if ($previous instanceof \Exception) { $e->setPrevious(static::create($previous)); } elseif ($previous instanceof \Throwable) { $e->setPrevious(static::create(new FatalThrowableError($previous))); } return $e; } public function toArray() { $exceptions = array(); foreach (array_merge(array($this), $this->getAllPrevious()) as $exception) { $exceptions[] = array( 'message' => $exception->getMessage(), 'class' => $exception->getClass(), 'trace' => $exception->getTrace(), ); } return $exceptions; } public function getStatusCode() { return $this->statusCode; } public function setStatusCode($code) { $this->statusCode = $code; } public function getHeaders() { return $this->headers; } public function setHeaders(array $headers) { $this->headers = $headers; } public function getClass() { return $this->class; } public function setClass($class) { $this->class = $class; } public function getFile() { return $this->file; } public function setFile($file) { $this->file = $file; } public function getLine() { return $this->line; } public function setLine($line) { $this->line = $line; } public function getMessage() { return $this->message; } public function setMessage($message) { $this->message = $message; } public function getCode() { return $this->code; } public function setCode($code) { $this->code = $code; } public function getPrevious() { return $this->previous; } public function setPrevious(FlattenException $previous) { $this->previous = $previous; } public function getAllPrevious() { $exceptions = array(); $e = $this; while ($e = $e->getPrevious()) { $exceptions[] = $e; } return $exceptions; } public function getTrace() { return $this->trace; } public function setTraceFromException(\Exception $exception) { $this->setTrace($exception->getTrace(), $exception->getFile(), $exception->getLine()); } public function setTrace($trace, $file, $line) { $this->trace = array(); $this->trace[] = array( 'namespace' => '', 'short_class' => '', 'class' => '', 'type' => '', 'function' => '', 'file' => $file, 'line' => $line, 'args' => array(), ); foreach ($trace as $entry) { $class = ''; $namespace = ''; if (isset($entry['class'])) { $parts = explode('\\', $entry['class']); $class = array_pop($parts); $namespace = implode('\\', $parts); } $this->trace[] = array( 'namespace' => $namespace, 'short_class' => $class, 'class' => isset($entry['class']) ? $entry['class'] : '', 'type' => isset($entry['type']) ? $entry['type'] : '', 'function' => isset($entry['function']) ? $entry['function'] : null, 'file' => isset($entry['file']) ? $entry['file'] : null, 'line' => isset($entry['line']) ? $entry['line'] : null, 'args' => isset($entry['args']) ? $this->flattenArgs($entry['args']) : array(), ); } } private function flattenArgs($args, $level = 0, &$count = 0) { $result = array(); foreach ($args as $key => $value) { if (++$count > 1e4) { return array('array', '*SKIPPED over 10000 entries*'); } if ($value instanceof \__PHP_Incomplete_Class) { // is_object() returns false on PHP<=7.1 $result[$key] = array('incomplete-object', $this->getClassNameFromIncomplete($value)); } elseif (is_object($value)) { $result[$key] = array('object', get_class($value)); } elseif (is_array($value)) { if ($level > 10) { $result[$key] = array('array', '*DEEP NESTED ARRAY*'); } else { $result[$key] = array('array', $this->flattenArgs($value, $level + 1, $count)); } } elseif (null === $value) { $result[$key] = array('null', null); } elseif (is_bool($value)) { $result[$key] = array('boolean', $value); } elseif (is_resource($value)) { $result[$key] = array('resource', get_resource_type($value)); } else { $result[$key] = array('string', (string) $value); } } return $result; } private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value) { $array = new \ArrayObject($value); return $array['__PHP_Incomplete_Class_Name']; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\Exception; /** * Out of memory exception. * * @author Nicolas Grekas */ class OutOfMemoryException extends FatalErrorException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\Exception; /** * Undefined Function Exception. * * @author Konstanton Myakshin */ class UndefinedFunctionException extends FatalErrorException { public function __construct($message, \ErrorException $previous) { parent::__construct( $message, $previous->getCode(), $previous->getSeverity(), $previous->getFile(), $previous->getLine(), $previous->getPrevious() ); $this->setTrace($previous->getTrace()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\Exception; /** * Undefined Method Exception. * * @author Grégoire Pineau */ class UndefinedMethodException extends FatalErrorException { public function __construct($message, \ErrorException $previous) { parent::__construct( $message, $previous->getCode(), $previous->getSeverity(), $previous->getFile(), $previous->getLine(), $previous->getPrevious() ); $this->setTrace($previous->getTrace()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\Debug\Exception\OutOfMemoryException; /** * ExceptionHandler converts an exception to a Response object. * * It is mostly useful in debug mode to replace the default PHP/XDebug * output with something prettier and more useful. * * As this class is mainly used during Kernel boot, where nothing is yet * available, the Response content is always HTML. * * @author Fabien Potencier * @author Nicolas Grekas */ class ExceptionHandler { private $debug; private $charset; private $handler; private $caughtBuffer; private $caughtLength; private $fileLinkFormat; public function __construct($debug = true, $charset = null, $fileLinkFormat = null) { if (false !== strpos($charset, '%')) { @trigger_error('Providing $fileLinkFormat as second argument to '.__METHOD__.' is deprecated since version 2.8 and will be unsupported in 3.0. Please provide it as third argument, after $charset.', E_USER_DEPRECATED); // Swap $charset and $fileLinkFormat for BC reasons $pivot = $fileLinkFormat; $fileLinkFormat = $charset; $charset = $pivot; } $this->debug = $debug; $this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8'; $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); } /** * Registers the exception handler. * * @param bool $debug Enable/disable debug mode, where the stack trace is displayed * @param string|null $charset The charset used by exception messages * @param string|null $fileLinkFormat The IDE link template * * @return ExceptionHandler The registered exception handler */ public static function register($debug = true, $charset = null, $fileLinkFormat = null) { $handler = new static($debug, $charset, $fileLinkFormat); $prev = set_exception_handler(array($handler, 'handle')); if (is_array($prev) && $prev[0] instanceof ErrorHandler) { restore_exception_handler(); $prev[0]->setExceptionHandler(array($handler, 'handle')); } return $handler; } /** * Sets a user exception handler. * * @param callable $handler An handler that will be called on Exception * * @return callable|null The previous exception handler if any */ public function setHandler($handler) { if (null !== $handler && !is_callable($handler)) { throw new \LogicException('The exception handler must be a valid PHP callable.'); } $old = $this->handler; $this->handler = $handler; return $old; } /** * Sets the format for links to source files. * * @param string $format The format for links to source files * * @return string The previous file link format */ public function setFileLinkFormat($format) { $old = $this->fileLinkFormat; $this->fileLinkFormat = $format; return $old; } /** * Sends a response for the given Exception. * * To be as fail-safe as possible, the exception is first handled * by our simple exception handler, then by the user exception handler. * The latter takes precedence and any output from the former is cancelled, * if and only if nothing bad happens in this handling path. */ public function handle(\Exception $exception) { if (null === $this->handler || $exception instanceof OutOfMemoryException) { $this->failSafeHandle($exception); return; } $caughtLength = $this->caughtLength = 0; ob_start(array($this, 'catchOutput')); $this->failSafeHandle($exception); while (null === $this->caughtBuffer && ob_end_flush()) { // Empty loop, everything is in the condition } if (isset($this->caughtBuffer[0])) { ob_start(array($this, 'cleanOutput')); echo $this->caughtBuffer; $caughtLength = ob_get_length(); } $this->caughtBuffer = null; try { call_user_func($this->handler, $exception); $this->caughtLength = $caughtLength; } catch (\Exception $e) { if (!$caughtLength) { // All handlers failed. Let PHP handle that now. throw $exception; } } } /** * Sends a response for the given Exception. * * If you have the Symfony HttpFoundation component installed, * this method will use it to create and send the response. If not, * it will fallback to plain PHP functions. * * @param \Exception $exception An \Exception instance */ private function failSafeHandle(\Exception $exception) { if (class_exists('Symfony\Component\HttpFoundation\Response', false) && __CLASS__ !== get_class($this) && ($reflector = new \ReflectionMethod($this, 'createResponse')) && __CLASS__ !== $reflector->class ) { $response = $this->createResponse($exception); $response->sendHeaders(); $response->sendContent(); @trigger_error(sprintf("The %s::createResponse method is deprecated since 2.8 and won't be called anymore when handling an exception in 3.0.", $reflector->class), E_USER_DEPRECATED); return; } $this->sendPhpResponse($exception); } /** * Sends the error associated with the given Exception as a plain PHP response. * * This method uses plain PHP functions like header() and echo to output * the response. * * @param \Exception|FlattenException $exception An \Exception or FlattenException instance */ public function sendPhpResponse($exception) { if (!$exception instanceof FlattenException) { $exception = FlattenException::create($exception); } if (!headers_sent()) { header(sprintf('HTTP/1.0 %s', $exception->getStatusCode())); foreach ($exception->getHeaders() as $name => $value) { header($name.': '.$value, false); } header('Content-Type: text/html; charset='.$this->charset); } echo $this->decorate($this->getContent($exception), $this->getStylesheet($exception)); } /** * Creates the error Response associated with the given Exception. * * @param \Exception|FlattenException $exception An \Exception or FlattenException instance * * @return Response A Response instance * * @deprecated since 2.8, to be removed in 3.0. */ public function createResponse($exception) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); if (!$exception instanceof FlattenException) { $exception = FlattenException::create($exception); } return Response::create($this->getHtml($exception), $exception->getStatusCode(), $exception->getHeaders())->setCharset($this->charset); } /** * Gets the full HTML content associated with the given exception. * * @param \Exception|FlattenException $exception An \Exception or FlattenException instance * * @return string The HTML content as a string */ public function getHtml($exception) { if (!$exception instanceof FlattenException) { $exception = FlattenException::create($exception); } return $this->decorate($this->getContent($exception), $this->getStylesheet($exception)); } /** * Gets the HTML content associated with the given exception. * * @param FlattenException $exception A FlattenException instance * * @return string The content as a string */ public function getContent(FlattenException $exception) { switch ($exception->getStatusCode()) { case 404: $title = 'Sorry, the page you are looking for could not be found.'; break; default: $title = 'Whoops, looks like something went wrong.'; } $content = ''; if ($this->debug) { try { $count = count($exception->getAllPrevious()); $total = $count + 1; foreach ($exception->toArray() as $position => $e) { $ind = $count - $position + 1; $class = $this->formatClass($e['class']); $message = nl2br($this->escapeHtml($e['message'])); $content .= sprintf(<<<'EOF'

    %d/%d %s%s: %s

      EOF , $ind, $total, $class, $this->formatPath($e['trace'][0]['file'], $e['trace'][0]['line']), $message); foreach ($e['trace'] as $trace) { $content .= '
    1. '; if ($trace['function']) { $content .= sprintf('at %s%s%s(%s)', $this->formatClass($trace['class']), $trace['type'], $trace['function'], $this->formatArgs($trace['args'])); } if (isset($trace['file']) && isset($trace['line'])) { $content .= $this->formatPath($trace['file'], $trace['line']); } $content .= "
    2. \n"; } $content .= "
    \n
    \n"; } } catch (\Exception $e) { // something nasty happened and we cannot throw an exception anymore if ($this->debug) { $title = sprintf('Exception thrown when handling an exception (%s: %s)', get_class($e), $this->escapeHtml($e->getMessage())); } else { $title = 'Whoops, looks like something went wrong.'; } } } return <<

    $title

    $content EOF; } /** * Gets the stylesheet associated with the given exception. * * @param FlattenException $exception A FlattenException instance * * @return string The stylesheet as a string */ public function getStylesheet(FlattenException $exception) { return <<<'EOF' .sf-reset { font: 11px Verdana, Arial, sans-serif; color: #333 } .sf-reset .clear { clear:both; height:0; font-size:0; line-height:0; } .sf-reset .clear_fix:after { display:block; height:0; clear:both; visibility:hidden; } .sf-reset .clear_fix { display:inline-block; } .sf-reset * html .clear_fix { height:1%; } .sf-reset .clear_fix { display:block; } .sf-reset, .sf-reset .block { margin: auto } .sf-reset abbr { border-bottom: 1px dotted #000; cursor: help; } .sf-reset p { font-size:14px; line-height:20px; color:#868686; padding-bottom:20px } .sf-reset strong { font-weight:bold; } .sf-reset a { color:#6c6159; cursor: default; } .sf-reset a img { border:none; } .sf-reset a:hover { text-decoration:underline; } .sf-reset em { font-style:italic; } .sf-reset h1, .sf-reset h2 { font: 20px Georgia, "Times New Roman", Times, serif } .sf-reset .exception_counter { background-color: #fff; color: #333; padding: 6px; float: left; margin-right: 10px; float: left; display: block; } .sf-reset .exception_title { margin-left: 3em; margin-bottom: 0.7em; display: block; } .sf-reset .exception_message { margin-left: 3em; display: block; } .sf-reset .traces li { font-size:12px; padding: 2px 4px; list-style-type:decimal; margin-left:20px; } .sf-reset .block { background-color:#FFFFFF; padding:10px 28px; margin-bottom:20px; -webkit-border-bottom-right-radius: 16px; -webkit-border-bottom-left-radius: 16px; -moz-border-radius-bottomright: 16px; -moz-border-radius-bottomleft: 16px; border-bottom-right-radius: 16px; border-bottom-left-radius: 16px; border-bottom:1px solid #ccc; border-right:1px solid #ccc; border-left:1px solid #ccc; word-wrap: break-word; } .sf-reset .block_exception { background-color:#ddd; color: #333; padding:20px; -webkit-border-top-left-radius: 16px; -webkit-border-top-right-radius: 16px; -moz-border-radius-topleft: 16px; -moz-border-radius-topright: 16px; border-top-left-radius: 16px; border-top-right-radius: 16px; border-top:1px solid #ccc; border-right:1px solid #ccc; border-left:1px solid #ccc; overflow: hidden; word-wrap: break-word; } .sf-reset a { background:none; color:#868686; text-decoration:none; } .sf-reset a:hover { background:none; color:#313131; text-decoration:underline; } .sf-reset ol { padding: 10px 0; } .sf-reset h1 { background-color:#FFFFFF; padding: 15px 28px; margin-bottom: 20px; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; border: 1px solid #ccc; } EOF; } private function decorate($content, $css) { return << $content EOF; } private function formatClass($class) { $parts = explode('\\', $class); return sprintf('%s', $class, array_pop($parts)); } private function formatPath($path, $line) { $path = $this->escapeHtml($path); $file = preg_match('#[^/\\\\]*$#', $path, $file) ? $file[0] : $path; if ($linkFormat = $this->fileLinkFormat) { $link = strtr($this->escapeHtml($linkFormat), array('%f' => $path, '%l' => (int) $line)); return sprintf(' in %s line %d', $link, $file, $line); } return sprintf(' in %s line %d', $path, $file, $line); } /** * Formats an array as a string. * * @param array $args The argument array * * @return string */ private function formatArgs(array $args) { $result = array(); foreach ($args as $key => $item) { if ('object' === $item[0]) { $formattedValue = sprintf('object(%s)', $this->formatClass($item[1])); } elseif ('array' === $item[0]) { $formattedValue = sprintf('array(%s)', is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); } elseif ('string' === $item[0]) { $formattedValue = sprintf("'%s'", $this->escapeHtml($item[1])); } elseif ('null' === $item[0]) { $formattedValue = 'null'; } elseif ('boolean' === $item[0]) { $formattedValue = ''.strtolower(var_export($item[1], true)).''; } elseif ('resource' === $item[0]) { $formattedValue = 'resource'; } else { $formattedValue = str_replace("\n", '', var_export($this->escapeHtml((string) $item[1]), true)); } $result[] = is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue); } return implode(', ', $result); } /** * Returns an UTF-8 and HTML encoded string. * * @deprecated since version 2.7, to be removed in 3.0. */ protected static function utf8Htmlize($str) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); return htmlspecialchars($str, ENT_QUOTES | (PHP_VERSION_ID >= 50400 ? ENT_SUBSTITUTE : 0), 'UTF-8'); } /** * HTML-encodes a string. */ private function escapeHtml($str) { return htmlspecialchars($str, ENT_QUOTES | (PHP_VERSION_ID >= 50400 ? ENT_SUBSTITUTE : 0), $this->charset); } /** * @internal */ public function catchOutput($buffer) { $this->caughtBuffer = $buffer; return ''; } /** * @internal */ public function cleanOutput($buffer) { if ($this->caughtLength) { // use substr_replace() instead of substr() for mbstring overloading resistance $cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength); if (isset($cleanBuffer[0])) { $buffer = $cleanBuffer; } } return $buffer; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\FatalErrorHandler; use Symfony\Component\Debug\Exception\ClassNotFoundException; use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\DebugClassLoader; use Composer\Autoload\ClassLoader as ComposerClassLoader; use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader; use Symfony\Component\ClassLoader\UniversalClassLoader as SymfonyUniversalClassLoader; /** * ErrorHandler for classes that do not exist. * * @author Fabien Potencier */ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface { /** * {@inheritdoc} */ public function handleError(array $error, FatalErrorException $exception) { $messageLen = strlen($error['message']); $notFoundSuffix = '\' not found'; $notFoundSuffixLen = strlen($notFoundSuffix); if ($notFoundSuffixLen > $messageLen) { return; } if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { return; } foreach (array('class', 'interface', 'trait') as $typeName) { $prefix = ucfirst($typeName).' \''; $prefixLen = strlen($prefix); if (0 !== strpos($error['message'], $prefix)) { continue; } $fullyQualifiedClassName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { $className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); $namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); $message = sprintf('Attempted to load %s "%s" from namespace "%s".', $typeName, $className, $namespacePrefix); $tail = ' for another namespace?'; } else { $className = $fullyQualifiedClassName; $message = sprintf('Attempted to load %s "%s" from the global namespace.', $typeName, $className); $tail = '?'; } if ($candidates = $this->getClassCandidates($className)) { $tail = array_pop($candidates).'"?'; if ($candidates) { $tail = ' for e.g. "'.implode('", "', $candidates).'" or "'.$tail; } else { $tail = ' for "'.$tail; } } $message .= "\nDid you forget a \"use\" statement".$tail; return new ClassNotFoundException($message, $exception); } } /** * Tries to guess the full namespace for a given class name. * * By default, it looks for PSR-0 and PSR-4 classes registered via a Symfony or a Composer * autoloader (that should cover all common cases). * * @param string $class A class name (without its namespace) * * @return array An array of possible fully qualified class names */ private function getClassCandidates($class) { if (!is_array($functions = spl_autoload_functions())) { return array(); } // find Symfony and Composer autoloaders $classes = array(); foreach ($functions as $function) { if (!is_array($function)) { continue; } // get class loaders wrapped by DebugClassLoader if ($function[0] instanceof DebugClassLoader) { $function = $function[0]->getClassLoader(); // @deprecated since version 2.5. Returning an object from DebugClassLoader::getClassLoader() is deprecated. if (is_object($function)) { $function = array($function); } if (!is_array($function)) { continue; } } if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader || $function[0] instanceof SymfonyUniversalClassLoader) { foreach ($function[0]->getPrefixes() as $prefix => $paths) { foreach ($paths as $path) { $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); } } } if ($function[0] instanceof ComposerClassLoader) { foreach ($function[0]->getPrefixesPsr4() as $prefix => $paths) { foreach ($paths as $path) { $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); } } } } return array_unique($classes); } /** * @param string $path * @param string $class * @param string $prefix * * @return array */ private function findClassInPath($path, $class, $prefix) { if (!$path = realpath($path.'/'.strtr($prefix, '\\_', '//')) ?: realpath($path.'/'.dirname(strtr($prefix, '\\_', '//'))) ?: realpath($path)) { return array(); } $classes = array(); $filename = $class.'.php'; foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { if ($filename == $file->getFileName() && $class = $this->convertFileToClass($path, $file->getPathName(), $prefix)) { $classes[] = $class; } } return $classes; } /** * @param string $path * @param string $file * @param string $prefix * * @return string|null */ private function convertFileToClass($path, $file, $prefix) { $candidates = array( // namespaced class $namespacedClass = str_replace(array($path.DIRECTORY_SEPARATOR, '.php', '/'), array('', '', '\\'), $file), // namespaced class (with target dir) $prefix.$namespacedClass, // namespaced class (with target dir and separator) $prefix.'\\'.$namespacedClass, // PEAR class str_replace('\\', '_', $namespacedClass), // PEAR class (with target dir) str_replace('\\', '_', $prefix.$namespacedClass), // PEAR class (with target dir and separator) str_replace('\\', '_', $prefix.'\\'.$namespacedClass), ); if ($prefix) { $candidates = array_filter($candidates, function ($candidate) use ($prefix) {return 0 === strpos($candidate, $prefix);}); } // We cannot use the autoloader here as most of them use require; but if the class // is not found, the new autoloader call will require the file again leading to a // "cannot redeclare class" error. foreach ($candidates as $candidate) { if ($this->classExists($candidate)) { return $candidate; } } require_once $file; foreach ($candidates as $candidate) { if ($this->classExists($candidate)) { return $candidate; } } } /** * @param string $class * * @return bool */ private function classExists($class) { return class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\FatalErrorHandler; use Symfony\Component\Debug\Exception\FatalErrorException; /** * Attempts to convert fatal errors to exceptions. * * @author Fabien Potencier */ interface FatalErrorHandlerInterface { /** * Attempts to convert an error into an exception. * * @param array $error An array as returned by error_get_last() * @param FatalErrorException $exception A FatalErrorException instance * * @return FatalErrorException|null A FatalErrorException instance if the class is able to convert the error, null otherwise */ public function handleError(array $error, FatalErrorException $exception); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\FatalErrorHandler; use Symfony\Component\Debug\Exception\UndefinedFunctionException; use Symfony\Component\Debug\Exception\FatalErrorException; /** * ErrorHandler for undefined functions. * * @author Fabien Potencier */ class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface { /** * {@inheritdoc} */ public function handleError(array $error, FatalErrorException $exception) { $messageLen = strlen($error['message']); $notFoundSuffix = '()'; $notFoundSuffixLen = strlen($notFoundSuffix); if ($notFoundSuffixLen > $messageLen) { return; } if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { return; } $prefix = 'Call to undefined function '; $prefixLen = strlen($prefix); if (0 !== strpos($error['message'], $prefix)) { return; } $fullyQualifiedFunctionName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedFunctionName, '\\')) { $functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1); $namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex); $message = sprintf('Attempted to call function "%s" from namespace "%s".', $functionName, $namespacePrefix); } else { $functionName = $fullyQualifiedFunctionName; $message = sprintf('Attempted to call function "%s" from the global namespace.', $functionName); } $candidates = array(); foreach (get_defined_functions() as $type => $definedFunctionNames) { foreach ($definedFunctionNames as $definedFunctionName) { if (false !== $namespaceSeparatorIndex = strrpos($definedFunctionName, '\\')) { $definedFunctionNameBasename = substr($definedFunctionName, $namespaceSeparatorIndex + 1); } else { $definedFunctionNameBasename = $definedFunctionName; } if ($definedFunctionNameBasename === $functionName) { $candidates[] = '\\'.$definedFunctionName; } } } if ($candidates) { sort($candidates); $last = array_pop($candidates).'"?'; if ($candidates) { $candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last; } else { $candidates = '"'.$last; } $message .= "\nDid you mean to call ".$candidates; } return new UndefinedFunctionException($message, $exception); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Debug\FatalErrorHandler; use Symfony\Component\Debug\Exception\FatalErrorException; use Symfony\Component\Debug\Exception\UndefinedMethodException; /** * ErrorHandler for undefined methods. * * @author Grégoire Pineau */ class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface { /** * {@inheritdoc} */ public function handleError(array $error, FatalErrorException $exception) { preg_match('/^Call to undefined method (.*)::(.*)\(\)$/', $error['message'], $matches); if (!$matches) { return; } $className = $matches[1]; $methodName = $matches[2]; $message = sprintf('Attempted to call an undefined method named "%s" of class "%s".', $methodName, $className); $candidates = array(); foreach (get_class_methods($className) as $definedMethodName) { $lev = levenshtein($methodName, $definedMethodName); if ($lev <= strlen($methodName) / 3 || false !== strpos($definedMethodName, $methodName)) { $candidates[] = $definedMethodName; } } if ($candidates) { sort($candidates); $last = array_pop($candidates).'"?'; if ($candidates) { $candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last; } else { $candidates = '"'.$last; } $message .= "\nDid you mean to call ".$candidates; } return new UndefinedMethodException($message, $exception); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Lazily loads listeners and subscribers from the dependency injection * container. * * @author Fabien Potencier * @author Bernhard Schussek * @author Jordan Alliot */ class ContainerAwareEventDispatcher extends EventDispatcher { /** * The container from where services are loaded. * * @var ContainerInterface */ private $container; /** * The service IDs of the event listeners and subscribers. * * @var array */ private $listenerIds = array(); /** * The services registered as listeners. * * @var array */ private $listeners = array(); /** * Constructor. * * @param ContainerInterface $container A ContainerInterface instance */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * Adds a service as event listener. * * @param string $eventName Event for which the listener is added * @param array $callback The service ID of the listener service & the method * name that has to be called * @param int $priority The higher this value, the earlier an event listener * will be triggered in the chain. * Defaults to 0. * * @throws \InvalidArgumentException */ public function addListenerService($eventName, $callback, $priority = 0) { if (!is_array($callback) || 2 !== count($callback)) { throw new \InvalidArgumentException('Expected an array("service", "method") argument'); } $this->listenerIds[$eventName][] = array($callback[0], $callback[1], $priority); } public function removeListener($eventName, $listener) { $this->lazyLoad($eventName); if (isset($this->listenerIds[$eventName])) { foreach ($this->listenerIds[$eventName] as $i => $args) { list($serviceId, $method, $priority) = $args; $key = $serviceId.'.'.$method; if (isset($this->listeners[$eventName][$key]) && $listener === array($this->listeners[$eventName][$key], $method)) { unset($this->listeners[$eventName][$key]); if (empty($this->listeners[$eventName])) { unset($this->listeners[$eventName]); } unset($this->listenerIds[$eventName][$i]); if (empty($this->listenerIds[$eventName])) { unset($this->listenerIds[$eventName]); } } } } parent::removeListener($eventName, $listener); } /** * {@inheritdoc} */ public function hasListeners($eventName = null) { if (null === $eventName) { return (bool) count($this->listenerIds) || (bool) count($this->listeners); } if (isset($this->listenerIds[$eventName])) { return true; } return parent::hasListeners($eventName); } /** * {@inheritdoc} */ public function getListeners($eventName = null) { if (null === $eventName) { foreach ($this->listenerIds as $serviceEventName => $args) { $this->lazyLoad($serviceEventName); } } else { $this->lazyLoad($eventName); } return parent::getListeners($eventName); } /** * {@inheritdoc} */ public function getListenerPriority($eventName, $listener) { $this->lazyLoad($eventName); return parent::getListenerPriority($eventName, $listener); } /** * Adds a service as event subscriber. * * @param string $serviceId The service ID of the subscriber service * @param string $class The service's class name (which must implement EventSubscriberInterface) */ public function addSubscriberService($serviceId, $class) { foreach ($class::getSubscribedEvents() as $eventName => $params) { if (is_string($params)) { $this->listenerIds[$eventName][] = array($serviceId, $params, 0); } elseif (is_string($params[0])) { $this->listenerIds[$eventName][] = array($serviceId, $params[0], isset($params[1]) ? $params[1] : 0); } else { foreach ($params as $listener) { $this->listenerIds[$eventName][] = array($serviceId, $listener[0], isset($listener[1]) ? $listener[1] : 0); } } } } public function getContainer() { return $this->container; } /** * Lazily loads listeners for this event from the dependency injection * container. * * @param string $eventName The name of the event to dispatch. The name of * the event is the name of the method that is * invoked on listeners. */ protected function lazyLoad($eventName) { if (isset($this->listenerIds[$eventName])) { foreach ($this->listenerIds[$eventName] as $args) { list($serviceId, $method, $priority) = $args; $listener = $this->container->get($serviceId); $key = $serviceId.'.'.$method; if (!isset($this->listeners[$eventName][$key])) { $this->addListener($eventName, array($listener, $method), $priority); } elseif ($listener !== $this->listeners[$eventName][$key]) { parent::removeListener($eventName, array($this->listeners[$eventName][$key], $method)); $this->addListener($eventName, array($listener, $method), $priority); } $this->listeners[$eventName][$key] = $listener; } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Debug; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\Stopwatch\Stopwatch; use Psr\Log\LoggerInterface; /** * Collects some data about event listeners. * * This event dispatcher delegates the dispatching to another one. * * @author Fabien Potencier */ class TraceableEventDispatcher implements TraceableEventDispatcherInterface { protected $logger; protected $stopwatch; private $called; private $dispatcher; private $wrappedListeners; /** * Constructor. * * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance * @param Stopwatch $stopwatch A Stopwatch instance * @param LoggerInterface $logger A LoggerInterface instance */ public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, LoggerInterface $logger = null) { $this->dispatcher = $dispatcher; $this->stopwatch = $stopwatch; $this->logger = $logger; $this->called = array(); $this->wrappedListeners = array(); } /** * {@inheritdoc} */ public function addListener($eventName, $listener, $priority = 0) { $this->dispatcher->addListener($eventName, $listener, $priority); } /** * {@inheritdoc} */ public function addSubscriber(EventSubscriberInterface $subscriber) { $this->dispatcher->addSubscriber($subscriber); } /** * {@inheritdoc} */ public function removeListener($eventName, $listener) { if (isset($this->wrappedListeners[$eventName])) { foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) { if ($wrappedListener->getWrappedListener() === $listener) { $listener = $wrappedListener; unset($this->wrappedListeners[$eventName][$index]); break; } } } return $this->dispatcher->removeListener($eventName, $listener); } /** * {@inheritdoc} */ public function removeSubscriber(EventSubscriberInterface $subscriber) { return $this->dispatcher->removeSubscriber($subscriber); } /** * {@inheritdoc} */ public function getListeners($eventName = null) { return $this->dispatcher->getListeners($eventName); } /** * {@inheritdoc} */ public function getListenerPriority($eventName, $listener) { if (!method_exists($this->dispatcher, 'getListenerPriority')) { return 0; } return $this->dispatcher->getListenerPriority($eventName, $listener); } /** * {@inheritdoc} */ public function hasListeners($eventName = null) { return $this->dispatcher->hasListeners($eventName); } /** * {@inheritdoc} */ public function dispatch($eventName, Event $event = null) { if (null === $event) { $event = new Event(); } if (null !== $this->logger && $event->isPropagationStopped()) { $this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName)); } $this->preProcess($eventName); $this->preDispatch($eventName, $event); $e = $this->stopwatch->start($eventName, 'section'); $this->dispatcher->dispatch($eventName, $event); if ($e->isStarted()) { $e->stop(); } $this->postDispatch($eventName, $event); $this->postProcess($eventName); return $event; } /** * {@inheritdoc} */ public function getCalledListeners() { $called = array(); foreach ($this->called as $eventName => $listeners) { foreach ($listeners as $listener) { $info = $this->getListenerInfo($listener->getWrappedListener(), $eventName); $called[$eventName.'.'.$info['pretty']] = $info; } } return $called; } /** * {@inheritdoc} */ public function getNotCalledListeners() { try { $allListeners = $this->getListeners(); } catch (\Exception $e) { if (null !== $this->logger) { $this->logger->info('An exception was thrown while getting the uncalled listeners.', array('exception' => $e)); } // unable to retrieve the uncalled listeners return array(); } $notCalled = array(); foreach ($allListeners as $eventName => $listeners) { foreach ($listeners as $listener) { $called = false; if (isset($this->called[$eventName])) { foreach ($this->called[$eventName] as $l) { if ($l->getWrappedListener() === $listener) { $called = true; break; } } } if (!$called) { $info = $this->getListenerInfo($listener, $eventName); $notCalled[$eventName.'.'.$info['pretty']] = $info; } } } uasort($notCalled, array($this, 'sortListenersByPriority')); return $notCalled; } /** * Proxies all method calls to the original event dispatcher. * * @param string $method The method name * @param array $arguments The method arguments * * @return mixed */ public function __call($method, $arguments) { return call_user_func_array(array($this->dispatcher, $method), $arguments); } /** * Called before dispatching the event. * * @param string $eventName The event name * @param Event $event The event */ protected function preDispatch($eventName, Event $event) { } /** * Called after dispatching the event. * * @param string $eventName The event name * @param Event $event The event */ protected function postDispatch($eventName, Event $event) { } private function preProcess($eventName) { foreach ($this->dispatcher->getListeners($eventName) as $listener) { $info = $this->getListenerInfo($listener, $eventName); $name = isset($info['class']) ? $info['class'] : $info['type']; $wrappedListener = new WrappedListener($listener, $name, $this->stopwatch, $this); $this->wrappedListeners[$eventName][] = $wrappedListener; $this->dispatcher->removeListener($eventName, $listener); $this->dispatcher->addListener($eventName, $wrappedListener, $info['priority']); } } private function postProcess($eventName) { unset($this->wrappedListeners[$eventName]); $skipped = false; foreach ($this->dispatcher->getListeners($eventName) as $listener) { if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch. continue; } // Unwrap listener $priority = $this->getListenerPriority($eventName, $listener); $this->dispatcher->removeListener($eventName, $listener); $this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority); $info = $this->getListenerInfo($listener->getWrappedListener(), $eventName); if ($listener->wasCalled()) { if (null !== $this->logger) { $this->logger->debug(sprintf('Notified event "%s" to listener "%s".', $eventName, $info['pretty'])); } if (!isset($this->called[$eventName])) { $this->called[$eventName] = new \SplObjectStorage(); } $this->called[$eventName]->attach($listener); } if (null !== $this->logger && $skipped) { $this->logger->debug(sprintf('Listener "%s" was not called for event "%s".', $info['pretty'], $eventName)); } if ($listener->stoppedPropagation()) { if (null !== $this->logger) { $this->logger->debug(sprintf('Listener "%s" stopped propagation of the event "%s".', $info['pretty'], $eventName)); } $skipped = true; } } } /** * Returns information about the listener. * * @param object $listener The listener * @param string $eventName The event name * * @return array Information about the listener */ private function getListenerInfo($listener, $eventName) { $info = array( 'event' => $eventName, 'priority' => $this->getListenerPriority($eventName, $listener), ); if ($listener instanceof \Closure) { $info += array( 'type' => 'Closure', 'pretty' => 'closure', ); } elseif (is_string($listener)) { try { $r = new \ReflectionFunction($listener); $file = $r->getFileName(); $line = $r->getStartLine(); } catch (\ReflectionException $e) { $file = null; $line = null; } $info += array( 'type' => 'Function', 'function' => $listener, 'file' => $file, 'line' => $line, 'pretty' => $listener, ); } elseif (is_array($listener) || (is_object($listener) && is_callable($listener))) { if (!is_array($listener)) { $listener = array($listener, '__invoke'); } $class = is_object($listener[0]) ? get_class($listener[0]) : $listener[0]; try { $r = new \ReflectionMethod($class, $listener[1]); $file = $r->getFileName(); $line = $r->getStartLine(); } catch (\ReflectionException $e) { $file = null; $line = null; } $info += array( 'type' => 'Method', 'class' => $class, 'method' => $listener[1], 'file' => $file, 'line' => $line, 'pretty' => $class.'::'.$listener[1], ); } return $info; } private function sortListenersByPriority($a, $b) { if (is_int($a['priority']) && !is_int($b['priority'])) { return 1; } if (!is_int($a['priority']) && is_int($b['priority'])) { return -1; } if ($a['priority'] === $b['priority']) { return 0; } if ($a['priority'] > $b['priority']) { return -1; } return 1; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Debug; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * @author Fabien Potencier */ interface TraceableEventDispatcherInterface extends EventDispatcherInterface { /** * Gets the called listeners. * * @return array An array of called listeners */ public function getCalledListeners(); /** * Gets the not called listeners. * * @return array An array of not called listeners */ public function getNotCalledListeners(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\Debug; use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\EventDispatcher\Event; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * @author Fabien Potencier */ class WrappedListener { private $listener; private $name; private $called; private $stoppedPropagation; private $stopwatch; private $dispatcher; public function __construct($listener, $name, Stopwatch $stopwatch, EventDispatcherInterface $dispatcher = null) { $this->listener = $listener; $this->name = $name; $this->stopwatch = $stopwatch; $this->dispatcher = $dispatcher; $this->called = false; $this->stoppedPropagation = false; } public function getWrappedListener() { return $this->listener; } public function wasCalled() { return $this->called; } public function stoppedPropagation() { return $this->stoppedPropagation; } public function __invoke(Event $event, $eventName, EventDispatcherInterface $dispatcher) { $this->called = true; $e = $this->stopwatch->start($this->name, 'event_listener'); call_user_func($this->listener, $event, $eventName, $this->dispatcher ?: $dispatcher); if ($e->isStarted()) { $e->stop(); } if ($event->isPropagationStopped()) { $this->stoppedPropagation = true; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; /** * Compiler pass to register tagged services for an event dispatcher. */ class RegisterListenersPass implements CompilerPassInterface { /** * @var string */ protected $dispatcherService; /** * @var string */ protected $listenerTag; /** * @var string */ protected $subscriberTag; /** * Constructor. * * @param string $dispatcherService Service name of the event dispatcher in processed container * @param string $listenerTag Tag name used for listener * @param string $subscriberTag Tag name used for subscribers */ public function __construct($dispatcherService = 'event_dispatcher', $listenerTag = 'kernel.event_listener', $subscriberTag = 'kernel.event_subscriber') { $this->dispatcherService = $dispatcherService; $this->listenerTag = $listenerTag; $this->subscriberTag = $subscriberTag; } public function process(ContainerBuilder $container) { if (!$container->hasDefinition($this->dispatcherService) && !$container->hasAlias($this->dispatcherService)) { return; } $definition = $container->findDefinition($this->dispatcherService); foreach ($container->findTaggedServiceIds($this->listenerTag) as $id => $events) { $def = $container->getDefinition($id); if (!$def->isPublic()) { throw new \InvalidArgumentException(sprintf('The service "%s" must be public as event listeners are lazy-loaded.', $id)); } if ($def->isAbstract()) { throw new \InvalidArgumentException(sprintf('The service "%s" must not be abstract as event listeners are lazy-loaded.', $id)); } foreach ($events as $event) { $priority = isset($event['priority']) ? $event['priority'] : 0; if (!isset($event['event'])) { throw new \InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "%s" tags.', $id, $this->listenerTag)); } if (!isset($event['method'])) { $event['method'] = 'on'.preg_replace_callback(array( '/(?<=\b)[a-z]/i', '/[^a-z0-9]/i', ), function ($matches) { return strtoupper($matches[0]); }, $event['event']); $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); } $definition->addMethodCall('addListenerService', array($event['event'], array($id, $event['method']), $priority)); } } foreach ($container->findTaggedServiceIds($this->subscriberTag) as $id => $attributes) { $def = $container->getDefinition($id); if (!$def->isPublic()) { throw new \InvalidArgumentException(sprintf('The service "%s" must be public as event subscribers are lazy-loaded.', $id)); } if ($def->isAbstract()) { throw new \InvalidArgumentException(sprintf('The service "%s" must not be abstract as event subscribers are lazy-loaded.', $id)); } // We must assume that the class value has been correctly filled, even if the service is created by a factory $class = $container->getParameterBag()->resolveValue($def->getClass()); $interface = 'Symfony\Component\EventDispatcher\EventSubscriberInterface'; if (!is_subclass_of($class, $interface)) { if (!class_exists($class, false)) { throw new \InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); } throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface)); } $definition->addMethodCall('addSubscriberService', array($id, $class)); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * Event is the base class for classes containing event data. * * This class contains no event data. It is used by events that do not pass * state information to an event handler when an event is raised. * * You can call the method stopPropagation() to abort the execution of * further listeners in your event listener. * * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel * @author Bernhard Schussek */ class Event { /** * @var bool Whether no further event listeners should be triggered */ private $propagationStopped = false; /** * @var EventDispatcher Dispatcher that dispatched this event */ private $dispatcher; /** * @var string This event's name */ private $name; /** * Returns whether further event listeners should be triggered. * * @see Event::stopPropagation() * * @return bool Whether propagation was already stopped for this event */ public function isPropagationStopped() { return $this->propagationStopped; } /** * Stops the propagation of the event to further event listeners. * * If multiple event listeners are connected to the same event, no * further event listener will be triggered once any trigger calls * stopPropagation(). */ public function stopPropagation() { $this->propagationStopped = true; } /** * Stores the EventDispatcher that dispatches this Event. * * @param EventDispatcherInterface $dispatcher * * @deprecated since version 2.4, to be removed in 3.0. The event dispatcher is passed to the listener call. */ public function setDispatcher(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } /** * Returns the EventDispatcher that dispatches this Event. * * @return EventDispatcherInterface * * @deprecated since version 2.4, to be removed in 3.0. The event dispatcher is passed to the listener call. */ public function getDispatcher() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.4 and will be removed in 3.0. The event dispatcher instance can be received in the listener call instead.', E_USER_DEPRECATED); return $this->dispatcher; } /** * Gets the event's name. * * @return string * * @deprecated since version 2.4, to be removed in 3.0. The event name is passed to the listener call. */ public function getName() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.4 and will be removed in 3.0. The event name can be received in the listener call instead.', E_USER_DEPRECATED); return $this->name; } /** * Sets the event's name property. * * @param string $name The event name * * @deprecated since version 2.4, to be removed in 3.0. The event name is passed to the listener call. */ public function setName($name) { $this->name = $name; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * The EventDispatcherInterface is the central point of Symfony's event listener system. * * Listeners are registered on the manager and events are dispatched through the * manager. * * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel * @author Bernhard Schussek * @author Fabien Potencier * @author Jordi Boggiano * @author Jordan Alliot */ class EventDispatcher implements EventDispatcherInterface { private $listeners = array(); private $sorted = array(); /** * {@inheritdoc} */ public function dispatch($eventName, Event $event = null) { if (null === $event) { $event = new Event(); } $event->setDispatcher($this); $event->setName($eventName); if ($listeners = $this->getListeners($eventName)) { $this->doDispatch($listeners, $eventName, $event); } return $event; } /** * {@inheritdoc} */ public function getListeners($eventName = null) { if (null !== $eventName) { if (!isset($this->listeners[$eventName])) { return array(); } if (!isset($this->sorted[$eventName])) { $this->sortListeners($eventName); } return $this->sorted[$eventName]; } foreach ($this->listeners as $eventName => $eventListeners) { if (!isset($this->sorted[$eventName])) { $this->sortListeners($eventName); } } return array_filter($this->sorted); } /** * Gets the listener priority for a specific event. * * Returns null if the event or the listener does not exist. * * @param string $eventName The name of the event * @param callable $listener The listener * * @return int|null The event listener priority */ public function getListenerPriority($eventName, $listener) { if (!isset($this->listeners[$eventName])) { return; } foreach ($this->listeners[$eventName] as $priority => $listeners) { if (false !== in_array($listener, $listeners, true)) { return $priority; } } } /** * {@inheritdoc} */ public function hasListeners($eventName = null) { return (bool) count($this->getListeners($eventName)); } /** * {@inheritdoc} */ public function addListener($eventName, $listener, $priority = 0) { $this->listeners[$eventName][$priority][] = $listener; unset($this->sorted[$eventName]); } /** * {@inheritdoc} */ public function removeListener($eventName, $listener) { if (!isset($this->listeners[$eventName])) { return; } foreach ($this->listeners[$eventName] as $priority => $listeners) { if (false !== ($key = array_search($listener, $listeners, true))) { unset($this->listeners[$eventName][$priority][$key], $this->sorted[$eventName]); } } } /** * {@inheritdoc} */ public function addSubscriber(EventSubscriberInterface $subscriber) { foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { if (is_string($params)) { $this->addListener($eventName, array($subscriber, $params)); } elseif (is_string($params[0])) { $this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0); } else { foreach ($params as $listener) { $this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0); } } } } /** * {@inheritdoc} */ public function removeSubscriber(EventSubscriberInterface $subscriber) { foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { if (is_array($params) && is_array($params[0])) { foreach ($params as $listener) { $this->removeListener($eventName, array($subscriber, $listener[0])); } } else { $this->removeListener($eventName, array($subscriber, is_string($params) ? $params : $params[0])); } } } /** * Triggers the listeners of an event. * * This method can be overridden to add functionality that is executed * for each listener. * * @param callable[] $listeners The event listeners * @param string $eventName The name of the event to dispatch * @param Event $event The event object to pass to the event handlers/listeners */ protected function doDispatch($listeners, $eventName, Event $event) { foreach ($listeners as $listener) { if ($event->isPropagationStopped()) { break; } call_user_func($listener, $event, $eventName, $this); } } /** * Sorts the internal list of listeners for the given event by priority. * * @param string $eventName The name of the event */ private function sortListeners($eventName) { krsort($this->listeners[$eventName]); $this->sorted[$eventName] = call_user_func_array('array_merge', $this->listeners[$eventName]); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * The EventDispatcherInterface is the central point of Symfony's event listener system. * Listeners are registered on the manager and events are dispatched through the * manager. * * @author Bernhard Schussek */ interface EventDispatcherInterface { /** * Dispatches an event to all registered listeners. * * @param string $eventName The name of the event to dispatch. The name of * the event is the name of the method that is * invoked on listeners. * @param Event $event The event to pass to the event handlers/listeners * If not supplied, an empty Event instance is created. * * @return Event */ public function dispatch($eventName, Event $event = null); /** * Adds an event listener that listens on the specified events. * * @param string $eventName The event to listen on * @param callable $listener The listener * @param int $priority The higher this value, the earlier an event * listener will be triggered in the chain (defaults to 0) */ public function addListener($eventName, $listener, $priority = 0); /** * Adds an event subscriber. * * The subscriber is asked for all the events he is * interested in and added as a listener for these events. * * @param EventSubscriberInterface $subscriber The subscriber */ public function addSubscriber(EventSubscriberInterface $subscriber); /** * Removes an event listener from the specified events. * * @param string $eventName The event to remove a listener from * @param callable $listener The listener to remove */ public function removeListener($eventName, $listener); /** * Removes an event subscriber. * * @param EventSubscriberInterface $subscriber The subscriber */ public function removeSubscriber(EventSubscriberInterface $subscriber); /** * Gets the listeners of a specific event or all listeners sorted by descending priority. * * @param string $eventName The name of the event * * @return array The event listeners for the specified event, or all event listeners by event name */ public function getListeners($eventName = null); /** * Checks whether an event has any registered listeners. * * @param string $eventName The name of the event * * @return bool true if the specified event has any listeners, false otherwise */ public function hasListeners($eventName = null); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * An EventSubscriber knows himself what events he is interested in. * If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes * {@link getSubscribedEvents} and registers the subscriber as a listener for all * returned events. * * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel * @author Bernhard Schussek */ interface EventSubscriberInterface { /** * 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(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * Event encapsulation class. * * Encapsulates events thus decoupling the observer from the subject they encapsulate. * * @author Drak */ class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate { /** * Event subject. * * @var mixed usually object or callable */ protected $subject; /** * Array of arguments. * * @var array */ protected $arguments; /** * Encapsulate an event with $subject and $args. * * @param mixed $subject The subject of the event, usually an object * @param array $arguments Arguments to store in the event */ public function __construct($subject = null, array $arguments = array()) { $this->subject = $subject; $this->arguments = $arguments; } /** * Getter for subject property. * * @return mixed $subject The observer subject */ public function getSubject() { return $this->subject; } /** * Get argument by key. * * @param string $key Key * * @return mixed Contents of array key * * @throws \InvalidArgumentException If key is not found. */ public function getArgument($key) { if ($this->hasArgument($key)) { return $this->arguments[$key]; } throw new \InvalidArgumentException(sprintf('Argument "%s" not found.', $key)); } /** * Add argument to event. * * @param string $key Argument name * @param mixed $value Value * * @return GenericEvent */ public function setArgument($key, $value) { $this->arguments[$key] = $value; return $this; } /** * Getter for all arguments. * * @return array */ public function getArguments() { return $this->arguments; } /** * Set args property. * * @param array $args Arguments * * @return GenericEvent */ public function setArguments(array $args = array()) { $this->arguments = $args; return $this; } /** * Has argument. * * @param string $key Key of arguments array * * @return bool */ public function hasArgument($key) { return array_key_exists($key, $this->arguments); } /** * ArrayAccess for argument getter. * * @param string $key Array key * * @return mixed * * @throws \InvalidArgumentException If key does not exist in $this->args. */ public function offsetGet($key) { return $this->getArgument($key); } /** * ArrayAccess for argument setter. * * @param string $key Array key to set * @param mixed $value Value */ public function offsetSet($key, $value) { $this->setArgument($key, $value); } /** * ArrayAccess for unset argument. * * @param string $key Array key */ public function offsetUnset($key) { if ($this->hasArgument($key)) { unset($this->arguments[$key]); } } /** * ArrayAccess has argument. * * @param string $key Array key * * @return bool */ public function offsetExists($key) { return $this->hasArgument($key); } /** * IteratorAggregate for iterating over the object like an array. * * @return \ArrayIterator */ public function getIterator() { return new \ArrayIterator($this->arguments); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\EventDispatcher; /** * A read-only proxy for an event dispatcher. * * @author Bernhard Schussek */ class ImmutableEventDispatcher implements EventDispatcherInterface { /** * The proxied dispatcher. * * @var EventDispatcherInterface */ private $dispatcher; /** * Creates an unmodifiable proxy for an event dispatcher. * * @param EventDispatcherInterface $dispatcher The proxied event dispatcher */ public function __construct(EventDispatcherInterface $dispatcher) { $this->dispatcher = $dispatcher; } /** * {@inheritdoc} */ public function dispatch($eventName, Event $event = null) { return $this->dispatcher->dispatch($eventName, $event); } /** * {@inheritdoc} */ public function addListener($eventName, $listener, $priority = 0) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } /** * {@inheritdoc} */ public function addSubscriber(EventSubscriberInterface $subscriber) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } /** * {@inheritdoc} */ public function removeListener($eventName, $listener) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } /** * {@inheritdoc} */ public function removeSubscriber(EventSubscriberInterface $subscriber) { throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); } /** * {@inheritdoc} */ public function getListeners($eventName = null) { return $this->dispatcher->getListeners($eventName); } /** * {@inheritdoc} */ public function getListenerPriority($eventName, $listener) { return $this->dispatcher->getListenerPriority($eventName, $listener); } /** * {@inheritdoc} */ public function hasListeners($eventName = null) { return $this->dispatcher->hasListeners($eventName); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Adapter; @trigger_error('The '.__NAMESPACE__.'\AbstractAdapter class is deprecated since version 2.8 and will be removed in 3.0. Use directly the Finder class instead.', E_USER_DEPRECATED); /** * Interface for finder engine implementations. * * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. Use Finder instead. */ abstract class AbstractAdapter implements AdapterInterface { protected $followLinks = false; protected $mode = 0; protected $minDepth = 0; protected $maxDepth = PHP_INT_MAX; protected $exclude = array(); protected $names = array(); protected $notNames = array(); protected $contains = array(); protected $notContains = array(); protected $sizes = array(); protected $dates = array(); protected $filters = array(); protected $sort = false; protected $paths = array(); protected $notPaths = array(); protected $ignoreUnreadableDirs = false; private static $areSupported = array(); /** * {@inheritdoc} */ public function isSupported() { $name = $this->getName(); if (!array_key_exists($name, self::$areSupported)) { self::$areSupported[$name] = $this->canBeUsed(); } return self::$areSupported[$name]; } /** * {@inheritdoc} */ public function setFollowLinks($followLinks) { $this->followLinks = $followLinks; return $this; } /** * {@inheritdoc} */ public function setMode($mode) { $this->mode = $mode; return $this; } /** * {@inheritdoc} */ public function setDepths(array $depths) { $this->minDepth = 0; $this->maxDepth = PHP_INT_MAX; foreach ($depths as $comparator) { switch ($comparator->getOperator()) { case '>': $this->minDepth = $comparator->getTarget() + 1; break; case '>=': $this->minDepth = $comparator->getTarget(); break; case '<': $this->maxDepth = $comparator->getTarget() - 1; break; case '<=': $this->maxDepth = $comparator->getTarget(); break; default: $this->minDepth = $this->maxDepth = $comparator->getTarget(); } } return $this; } /** * {@inheritdoc} */ public function setExclude(array $exclude) { $this->exclude = $exclude; return $this; } /** * {@inheritdoc} */ public function setNames(array $names) { $this->names = $names; return $this; } /** * {@inheritdoc} */ public function setNotNames(array $notNames) { $this->notNames = $notNames; return $this; } /** * {@inheritdoc} */ public function setContains(array $contains) { $this->contains = $contains; return $this; } /** * {@inheritdoc} */ public function setNotContains(array $notContains) { $this->notContains = $notContains; return $this; } /** * {@inheritdoc} */ public function setSizes(array $sizes) { $this->sizes = $sizes; return $this; } /** * {@inheritdoc} */ public function setDates(array $dates) { $this->dates = $dates; return $this; } /** * {@inheritdoc} */ public function setFilters(array $filters) { $this->filters = $filters; return $this; } /** * {@inheritdoc} */ public function setSort($sort) { $this->sort = $sort; return $this; } /** * {@inheritdoc} */ public function setPath(array $paths) { $this->paths = $paths; return $this; } /** * {@inheritdoc} */ public function setNotPath(array $notPaths) { $this->notPaths = $notPaths; return $this; } /** * {@inheritdoc} */ public function ignoreUnreadableDirs($ignore = true) { $this->ignoreUnreadableDirs = (bool) $ignore; return $this; } /** * Returns whether the adapter is supported in the current environment. * * This method should be implemented in all adapters. Do not implement * isSupported in the adapters as the generic implementation provides a cache * layer. * * @see isSupported() * * @return bool Whether the adapter is supported */ abstract protected function canBeUsed(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Adapter; @trigger_error('The '.__NAMESPACE__.'\AbstractFindAdapter class is deprecated since version 2.8 and will be removed in 3.0. Use directly the Finder class instead.', E_USER_DEPRECATED); use Symfony\Component\Finder\Exception\AccessDeniedException; use Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Shell\Shell; use Symfony\Component\Finder\Expression\Expression; use Symfony\Component\Finder\Shell\Command; use Symfony\Component\Finder\Comparator\NumberComparator; use Symfony\Component\Finder\Comparator\DateComparator; /** * Shell engine implementation using GNU find command. * * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. Use Finder instead. */ abstract class AbstractFindAdapter extends AbstractAdapter { /** * @var Shell */ protected $shell; /** * Constructor. */ public function __construct() { $this->shell = new Shell(); } /** * {@inheritdoc} */ public function searchInDirectory($dir) { // having "/../" in path make find fail $dir = realpath($dir); // searching directories containing or not containing strings leads to no result if (Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES === $this->mode && ($this->contains || $this->notContains)) { return new Iterator\FilePathsIterator(array(), $dir); } $command = Command::create(); $find = $this->buildFindCommand($command, $dir); if ($this->followLinks) { $find->add('-follow'); } $find->add('-mindepth')->add($this->minDepth + 1); if (PHP_INT_MAX !== $this->maxDepth) { $find->add('-maxdepth')->add($this->maxDepth + 1); } if (Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES === $this->mode) { $find->add('-type d'); } elseif (Iterator\FileTypeFilterIterator::ONLY_FILES === $this->mode) { $find->add('-type f'); } $this->buildNamesFiltering($find, $this->names); $this->buildNamesFiltering($find, $this->notNames, true); $this->buildPathsFiltering($find, $dir, $this->paths); $this->buildPathsFiltering($find, $dir, $this->notPaths, true); $this->buildSizesFiltering($find, $this->sizes); $this->buildDatesFiltering($find, $this->dates); $useGrep = $this->shell->testCommand('grep') && $this->shell->testCommand('xargs'); $useSort = is_int($this->sort) && $this->shell->testCommand('sort') && $this->shell->testCommand('cut'); if ($useGrep && ($this->contains || $this->notContains)) { $grep = $command->ins('grep'); $this->buildContentFiltering($grep, $this->contains); $this->buildContentFiltering($grep, $this->notContains, true); } if ($useSort) { $this->buildSorting($command, $this->sort); } $command->setErrorHandler( $this->ignoreUnreadableDirs // If directory is unreadable and finder is set to ignore it, `stderr` is ignored. ? function ($stderr) { } : function ($stderr) { throw new AccessDeniedException($stderr); } ); $paths = $this->shell->testCommand('uniq') ? $command->add('| uniq')->execute() : array_unique($command->execute()); $iterator = new Iterator\FilePathsIterator($paths, $dir); if ($this->exclude) { $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude); } if (!$useGrep && ($this->contains || $this->notContains)) { $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains); } if ($this->filters) { $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters); } if (!$useSort && $this->sort) { $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort); $iterator = $iteratorAggregate->getIterator(); } return $iterator; } /** * {@inheritdoc} */ protected function canBeUsed() { return $this->shell->testCommand('find'); } /** * @param Command $command * @param string $dir * * @return Command */ protected function buildFindCommand(Command $command, $dir) { return $command ->ins('find') ->add('find ') ->arg($dir) ->add('-noleaf'); // the -noleaf option is required for filesystems that don't follow the '.' and '..' conventions } /** * @param Command $command * @param string[] $names * @param bool $not */ private function buildNamesFiltering(Command $command, array $names, $not = false) { if (0 === count($names)) { return; } $command->add($not ? '-not' : null)->cmd('('); foreach ($names as $i => $name) { $expr = Expression::create($name); // Find does not support expandable globs ("*.{a,b}" syntax). if ($expr->isGlob() && $expr->getGlob()->isExpandable()) { $expr = Expression::create($expr->getGlob()->toRegex(false)); } // Fixes 'not search' and 'full path matching' regex problems. // - Jokers '.' are replaced by [^/]. // - We add '[^/]*' before and after regex (if no ^|$ flags are present). if ($expr->isRegex()) { $regex = $expr->getRegex(); $regex->prepend($regex->hasStartFlag() ? '/' : '/[^/]*') ->setStartFlag(false) ->setStartJoker(true) ->replaceJokers('[^/]'); if (!$regex->hasEndFlag() || $regex->hasEndJoker()) { $regex->setEndJoker(false)->append('[^/]*'); } } $command ->add($i > 0 ? '-or' : null) ->add($expr->isRegex() ? ($expr->isCaseSensitive() ? '-regex' : '-iregex') : ($expr->isCaseSensitive() ? '-name' : '-iname') ) ->arg($expr->renderPattern()); } $command->cmd(')'); } /** * @param Command $command * @param string $dir * @param string[] $paths * @param bool $not */ private function buildPathsFiltering(Command $command, $dir, array $paths, $not = false) { if (0 === count($paths)) { return; } $command->add($not ? '-not' : null)->cmd('('); foreach ($paths as $i => $path) { $expr = Expression::create($path); // Find does not support expandable globs ("*.{a,b}" syntax). if ($expr->isGlob() && $expr->getGlob()->isExpandable()) { $expr = Expression::create($expr->getGlob()->toRegex(false)); } // Fixes 'not search' regex problems. if ($expr->isRegex()) { $regex = $expr->getRegex(); $regex->prepend($regex->hasStartFlag() ? preg_quote($dir).DIRECTORY_SEPARATOR : '.*')->setEndJoker(!$regex->hasEndFlag()); } else { $expr->prepend('*')->append('*'); } $command ->add($i > 0 ? '-or' : null) ->add($expr->isRegex() ? ($expr->isCaseSensitive() ? '-regex' : '-iregex') : ($expr->isCaseSensitive() ? '-path' : '-ipath') ) ->arg($expr->renderPattern()); } $command->cmd(')'); } /** * @param Command $command * @param NumberComparator[] $sizes */ private function buildSizesFiltering(Command $command, array $sizes) { foreach ($sizes as $i => $size) { $command->add($i > 0 ? '-and' : null); switch ($size->getOperator()) { case '<=': $command->add('-size -'.($size->getTarget() + 1).'c'); break; case '>=': $command->add('-size +'.($size->getTarget() - 1).'c'); break; case '>': $command->add('-size +'.$size->getTarget().'c'); break; case '!=': $command->add('-size -'.$size->getTarget().'c'); $command->add('-size +'.$size->getTarget().'c'); break; case '<': default: $command->add('-size -'.$size->getTarget().'c'); } } } /** * @param Command $command * @param DateComparator[] $dates */ private function buildDatesFiltering(Command $command, array $dates) { foreach ($dates as $i => $date) { $command->add($i > 0 ? '-and' : null); $mins = (int) round((time() - $date->getTarget()) / 60); if (0 > $mins) { // mtime is in the future $command->add(' -mmin -0'); // we will have no result so we don't need to continue return; } switch ($date->getOperator()) { case '<=': $command->add('-mmin +'.($mins - 1)); break; case '>=': $command->add('-mmin -'.($mins + 1)); break; case '>': $command->add('-mmin -'.$mins); break; case '!=': $command->add('-mmin +'.$mins.' -or -mmin -'.$mins); break; case '<': default: $command->add('-mmin +'.$mins); } } } /** * @param Command $command * @param string $sort * * @throws \InvalidArgumentException */ private function buildSorting(Command $command, $sort) { $this->buildFormatSorting($command, $sort); } /** * @param Command $command * @param string $sort */ abstract protected function buildFormatSorting(Command $command, $sort); /** * @param Command $command * @param array $contains * @param bool $not */ abstract protected function buildContentFiltering(Command $command, array $contains, $not = false); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Adapter; /** * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. */ interface AdapterInterface { /** * @param bool $followLinks * * @return AdapterInterface Current instance */ public function setFollowLinks($followLinks); /** * @param int $mode * * @return AdapterInterface Current instance */ public function setMode($mode); /** * @param array $exclude * * @return AdapterInterface Current instance */ public function setExclude(array $exclude); /** * @param array $depths * * @return AdapterInterface Current instance */ public function setDepths(array $depths); /** * @param array $names * * @return AdapterInterface Current instance */ public function setNames(array $names); /** * @param array $notNames * * @return AdapterInterface Current instance */ public function setNotNames(array $notNames); /** * @param array $contains * * @return AdapterInterface Current instance */ public function setContains(array $contains); /** * @param array $notContains * * @return AdapterInterface Current instance */ public function setNotContains(array $notContains); /** * @param array $sizes * * @return AdapterInterface Current instance */ public function setSizes(array $sizes); /** * @param array $dates * * @return AdapterInterface Current instance */ public function setDates(array $dates); /** * @param array $filters * * @return AdapterInterface Current instance */ public function setFilters(array $filters); /** * @param \Closure|int $sort * * @return AdapterInterface Current instance */ public function setSort($sort); /** * @param array $paths * * @return AdapterInterface Current instance */ public function setPath(array $paths); /** * @param array $notPaths * * @return AdapterInterface Current instance */ public function setNotPath(array $notPaths); /** * @param bool $ignore * * @return AdapterInterface Current instance */ public function ignoreUnreadableDirs($ignore = true); /** * @param string $dir * * @return \Iterator Result iterator */ public function searchInDirectory($dir); /** * Tests adapter support for current platform. * * @return bool */ public function isSupported(); /** * Returns adapter name. * * @return string */ public function getName(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Adapter; @trigger_error('The '.__NAMESPACE__.'\BsdFindAdapter class is deprecated since version 2.8 and will be removed in 3.0. Use directly the Finder class instead.', E_USER_DEPRECATED); use Symfony\Component\Finder\Shell\Shell; use Symfony\Component\Finder\Shell\Command; use Symfony\Component\Finder\Iterator\SortableIterator; use Symfony\Component\Finder\Expression\Expression; /** * Shell engine implementation using BSD find command. * * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. Use Finder instead. */ class BsdFindAdapter extends AbstractFindAdapter { /** * {@inheritdoc} */ public function getName() { return 'bsd_find'; } /** * {@inheritdoc} */ protected function canBeUsed() { return in_array($this->shell->getType(), array(Shell::TYPE_BSD, Shell::TYPE_DARWIN)) && parent::canBeUsed(); } /** * {@inheritdoc} */ protected function buildFormatSorting(Command $command, $sort) { switch ($sort) { case SortableIterator::SORT_BY_NAME: $command->ins('sort')->add('| sort'); return; case SortableIterator::SORT_BY_TYPE: $format = '%HT'; break; case SortableIterator::SORT_BY_ACCESSED_TIME: $format = '%a'; break; case SortableIterator::SORT_BY_CHANGED_TIME: $format = '%c'; break; case SortableIterator::SORT_BY_MODIFIED_TIME: $format = '%m'; break; default: throw new \InvalidArgumentException(sprintf('Unknown sort options: %s.', $sort)); } $command ->add('-print0 | xargs -0 stat -f') ->arg($format.'%t%N') ->add('| sort | cut -f 2'); } /** * {@inheritdoc} */ protected function buildFindCommand(Command $command, $dir) { parent::buildFindCommand($command, $dir)->addAtIndex('-E', 1); return $command; } /** * {@inheritdoc} */ protected function buildContentFiltering(Command $command, array $contains, $not = false) { foreach ($contains as $contain) { $expr = Expression::create($contain); // todo: avoid forking process for each $pattern by using multiple -e options $command ->add('| grep -v \'^$\'') ->add('| xargs -I{} grep -I') ->add($expr->isCaseSensitive() ? null : '-i') ->add($not ? '-L' : '-l') ->add('-Ee')->arg($expr->renderPattern()) ->add('{}') ; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Adapter; @trigger_error('The '.__NAMESPACE__.'\GnuFindAdapter class is deprecated since version 2.8 and will be removed in 3.0. Use directly the Finder class instead.', E_USER_DEPRECATED); use Symfony\Component\Finder\Shell\Shell; use Symfony\Component\Finder\Shell\Command; use Symfony\Component\Finder\Iterator\SortableIterator; use Symfony\Component\Finder\Expression\Expression; /** * Shell engine implementation using GNU find command. * * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. Use Finder instead. */ class GnuFindAdapter extends AbstractFindAdapter { /** * {@inheritdoc} */ public function getName() { return 'gnu_find'; } /** * {@inheritdoc} */ protected function buildFormatSorting(Command $command, $sort) { switch ($sort) { case SortableIterator::SORT_BY_NAME: $command->ins('sort')->add('| sort'); return; case SortableIterator::SORT_BY_TYPE: $format = '%y'; break; case SortableIterator::SORT_BY_ACCESSED_TIME: $format = '%A@'; break; case SortableIterator::SORT_BY_CHANGED_TIME: $format = '%C@'; break; case SortableIterator::SORT_BY_MODIFIED_TIME: $format = '%T@'; break; default: throw new \InvalidArgumentException(sprintf('Unknown sort options: %s.', $sort)); } $command ->get('find') ->add('-printf') ->arg($format.' %h/%f\\n') ->add('| sort | cut') ->arg('-d ') ->arg('-f2-') ; } /** * {@inheritdoc} */ protected function canBeUsed() { return $this->shell->getType() === Shell::TYPE_UNIX && parent::canBeUsed(); } /** * {@inheritdoc} */ protected function buildFindCommand(Command $command, $dir) { return parent::buildFindCommand($command, $dir)->add('-regextype posix-extended'); } /** * {@inheritdoc} */ protected function buildContentFiltering(Command $command, array $contains, $not = false) { foreach ($contains as $contain) { $expr = Expression::create($contain); // todo: avoid forking process for each $pattern by using multiple -e options $command ->add('| xargs -I{} -r grep -I') ->add($expr->isCaseSensitive() ? null : '-i') ->add($not ? '-L' : '-l') ->add('-Ee')->arg($expr->renderPattern()) ->add('{}') ; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Adapter; @trigger_error('The '.__NAMESPACE__.'\PhpAdapter class is deprecated since version 2.8 and will be removed in 3.0. Use directly the Finder class instead.', E_USER_DEPRECATED); use Symfony\Component\Finder\Iterator; /** * PHP finder engine implementation. * * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. Use Finder instead. */ class PhpAdapter extends AbstractAdapter { /** * {@inheritdoc} */ public function searchInDirectory($dir) { $flags = \RecursiveDirectoryIterator::SKIP_DOTS; if ($this->followLinks) { $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; } $iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs); if ($this->exclude) { $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude); } $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); if ($this->minDepth > 0 || $this->maxDepth < PHP_INT_MAX) { $iterator = new Iterator\DepthRangeFilterIterator($iterator, $this->minDepth, $this->maxDepth); } if ($this->mode) { $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode); } if ($this->names || $this->notNames) { $iterator = new Iterator\FilenameFilterIterator($iterator, $this->names, $this->notNames); } if ($this->contains || $this->notContains) { $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains); } if ($this->sizes) { $iterator = new Iterator\SizeRangeFilterIterator($iterator, $this->sizes); } if ($this->dates) { $iterator = new Iterator\DateRangeFilterIterator($iterator, $this->dates); } if ($this->filters) { $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters); } if ($this->paths || $this->notPaths) { $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $this->notPaths); } if ($this->sort) { $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort); $iterator = $iteratorAggregate->getIterator(); } return $iterator; } /** * {@inheritdoc} */ public function getName() { return 'php'; } /** * {@inheritdoc} */ protected function canBeUsed() { return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Comparator; /** * Comparator. * * @author Fabien Potencier */ class Comparator { private $target; private $operator = '=='; /** * Gets the target value. * * @return string The target value */ public function getTarget() { return $this->target; } /** * Sets the target value. * * @param string $target The target value */ public function setTarget($target) { $this->target = $target; } /** * Gets the comparison operator. * * @return string The operator */ public function getOperator() { return $this->operator; } /** * Sets the comparison operator. * * @param string $operator A valid operator * * @throws \InvalidArgumentException */ public function setOperator($operator) { if (!$operator) { $operator = '=='; } if (!in_array($operator, array('>', '<', '>=', '<=', '==', '!='))) { throw new \InvalidArgumentException(sprintf('Invalid operator "%s".', $operator)); } $this->operator = $operator; } /** * Tests against the target. * * @param mixed $test A test value * * @return bool */ public function test($test) { switch ($this->operator) { case '>': return $test > $this->target; case '>=': return $test >= $this->target; case '<': return $test < $this->target; case '<=': return $test <= $this->target; case '!=': return $test != $this->target; } return $test == $this->target; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Comparator; /** * DateCompare compiles date comparisons. * * @author Fabien Potencier */ class DateComparator extends Comparator { /** * Constructor. * * @param string $test A comparison string * * @throws \InvalidArgumentException If the test is not understood */ public function __construct($test) { if (!preg_match('#^\s*(==|!=|[<>]=?|after|since|before|until)?\s*(.+?)\s*$#i', $test, $matches)) { throw new \InvalidArgumentException(sprintf('Don\'t understand "%s" as a date test.', $test)); } try { $date = new \DateTime($matches[2]); $target = $date->format('U'); } catch (\Exception $e) { throw new \InvalidArgumentException(sprintf('"%s" is not a valid date.', $matches[2])); } $operator = isset($matches[1]) ? $matches[1] : '=='; if ('since' === $operator || 'after' === $operator) { $operator = '>'; } if ('until' === $operator || 'before' === $operator) { $operator = '<'; } $this->setOperator($operator); $this->setTarget($target); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Comparator; /** * NumberComparator compiles a simple comparison to an anonymous * subroutine, which you can call with a value to be tested again. * * Now this would be very pointless, if NumberCompare didn't understand * magnitudes. * * The target value may use magnitudes of kilobytes (k, ki), * megabytes (m, mi), or gigabytes (g, gi). Those suffixed * with an i use the appropriate 2**n version in accordance with the * IEC standard: http://physics.nist.gov/cuu/Units/binary.html * * Based on the Perl Number::Compare module. * * @author Fabien Potencier PHP port * @author Richard Clamp Perl version * @copyright 2004-2005 Fabien Potencier * @copyright 2002 Richard Clamp * * @see http://physics.nist.gov/cuu/Units/binary.html */ class NumberComparator extends Comparator { /** * Constructor. * * @param string|int $test A comparison string or an integer * * @throws \InvalidArgumentException If the test is not understood */ public function __construct($test) { if (!preg_match('#^\s*(==|!=|[<>]=?)?\s*([0-9\.]+)\s*([kmg]i?)?\s*$#i', $test, $matches)) { throw new \InvalidArgumentException(sprintf('Don\'t understand "%s" as a number test.', $test)); } $target = $matches[2]; if (!is_numeric($target)) { throw new \InvalidArgumentException(sprintf('Invalid number "%s".', $target)); } if (isset($matches[3])) { // magnitude switch (strtolower($matches[3])) { case 'k': $target *= 1000; break; case 'ki': $target *= 1024; break; case 'm': $target *= 1000000; break; case 'mi': $target *= 1024 * 1024; break; case 'g': $target *= 1000000000; break; case 'gi': $target *= 1024 * 1024 * 1024; break; } } $this->setTarget($target); $this->setOperator(isset($matches[1]) ? $matches[1] : '=='); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Exception; /** * @author Jean-François Simon */ class AccessDeniedException extends \UnexpectedValueException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Exception; @trigger_error('The '.__NAMESPACE__.'\AdapterFailureException class is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); use Symfony\Component\Finder\Adapter\AdapterInterface; /** * Base exception for all adapter failures. * * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. */ class AdapterFailureException extends \RuntimeException implements ExceptionInterface { /** * @var \Symfony\Component\Finder\Adapter\AdapterInterface */ private $adapter; /** * @param AdapterInterface $adapter * @param string|null $message * @param \Exception|null $previous */ public function __construct(AdapterInterface $adapter, $message = null, \Exception $previous = null) { $this->adapter = $adapter; parent::__construct($message ?: 'Search failed with "'.$adapter->getName().'" adapter.', $previous); } /** * {@inheritdoc} */ public function getAdapter() { return $this->adapter; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Exception; /** * @author Jean-François Simon */ interface ExceptionInterface { /** * @return \Symfony\Component\Finder\Adapter\AdapterInterface */ public function getAdapter(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Exception; @trigger_error('The '.__NAMESPACE__.'\OperationNotPermitedException class is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); /** * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. */ class OperationNotPermitedException extends AdapterFailureException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Exception; @trigger_error('The '.__NAMESPACE__.'\ShellCommandFailureException class is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); use Symfony\Component\Finder\Adapter\AdapterInterface; use Symfony\Component\Finder\Shell\Command; /** * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. */ class ShellCommandFailureException extends AdapterFailureException { /** * @var Command */ private $command; /** * @param AdapterInterface $adapter * @param Command $command * @param \Exception|null $previous */ public function __construct(AdapterInterface $adapter, Command $command, \Exception $previous = null) { $this->command = $command; parent::__construct($adapter, 'Shell command failed: "'.$command->join().'".', $previous); } /** * @return Command */ public function getCommand() { return $this->command; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Expression; @trigger_error('The '.__NAMESPACE__.'\Expression class is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); /** * @author Jean-François Simon */ class Expression implements ValueInterface { const TYPE_REGEX = 1; const TYPE_GLOB = 2; /** * @var ValueInterface */ private $value; /** * @param string $expr * * @return Expression */ public static function create($expr) { return new self($expr); } /** * @param string $expr */ public function __construct($expr) { try { $this->value = Regex::create($expr); } catch (\InvalidArgumentException $e) { $this->value = new Glob($expr); } } /** * @return string */ public function __toString() { return $this->render(); } /** * {@inheritdoc} */ public function render() { return $this->value->render(); } /** * {@inheritdoc} */ public function renderPattern() { return $this->value->renderPattern(); } /** * @return bool */ public function isCaseSensitive() { return $this->value->isCaseSensitive(); } /** * @return int */ public function getType() { return $this->value->getType(); } /** * {@inheritdoc} */ public function prepend($expr) { $this->value->prepend($expr); return $this; } /** * {@inheritdoc} */ public function append($expr) { $this->value->append($expr); return $this; } /** * @return bool */ public function isRegex() { return self::TYPE_REGEX === $this->value->getType(); } /** * @return bool */ public function isGlob() { return self::TYPE_GLOB === $this->value->getType(); } /** * @return Glob * * @throws \LogicException */ public function getGlob() { if (self::TYPE_GLOB !== $this->value->getType()) { throw new \LogicException('Regex can\'t be transformed to glob.'); } return $this->value; } /** * @return Regex */ public function getRegex() { return self::TYPE_REGEX === $this->value->getType() ? $this->value : $this->value->toRegex(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Expression; @trigger_error('The '.__NAMESPACE__.'\Glob class is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); use Symfony\Component\Finder\Glob as FinderGlob; /** * @author Jean-François Simon */ class Glob implements ValueInterface { /** * @var string */ private $pattern; /** * @param string $pattern */ public function __construct($pattern) { $this->pattern = $pattern; } /** * {@inheritdoc} */ public function render() { return $this->pattern; } /** * {@inheritdoc} */ public function renderPattern() { return $this->pattern; } /** * {@inheritdoc} */ public function getType() { return Expression::TYPE_GLOB; } /** * {@inheritdoc} */ public function isCaseSensitive() { return true; } /** * {@inheritdoc} */ public function prepend($expr) { $this->pattern = $expr.$this->pattern; return $this; } /** * {@inheritdoc} */ public function append($expr) { $this->pattern .= $expr; return $this; } /** * Tests if glob is expandable ("*.{a,b}" syntax). * * @return bool */ public function isExpandable() { return false !== strpos($this->pattern, '{') && false !== strpos($this->pattern, '}'); } /** * @param bool $strictLeadingDot * @param bool $strictWildcardSlash * * @return Regex */ public function toRegex($strictLeadingDot = true, $strictWildcardSlash = true) { $regex = FinderGlob::toRegex($this->pattern, $strictLeadingDot, $strictWildcardSlash, ''); return new Regex($regex); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Expression; @trigger_error('The '.__NAMESPACE__.'\Regex class is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); /** * @author Jean-François Simon */ class Regex implements ValueInterface { const START_FLAG = '^'; const END_FLAG = '$'; const BOUNDARY = '~'; const JOKER = '.*'; const ESCAPING = '\\'; /** * @var string */ private $pattern; /** * @var array */ private $options; /** * @var bool */ private $startFlag; /** * @var bool */ private $endFlag; /** * @var bool */ private $startJoker; /** * @var bool */ private $endJoker; /** * @param string $expr * * @return Regex * * @throws \InvalidArgumentException */ public static function create($expr) { if (preg_match('/^(.{3,}?)([imsxuADU]*)$/', $expr, $m)) { $start = substr($m[1], 0, 1); $end = substr($m[1], -1); if ( ($start === $end && !preg_match('/[*?[:alnum:] \\\\]/', $start)) || ($start === '{' && $end === '}') || ($start === '(' && $end === ')') ) { return new self(substr($m[1], 1, -1), $m[2], $end); } } throw new \InvalidArgumentException('Given expression is not a regex.'); } /** * @param string $pattern * @param string $options * @param string $delimiter */ public function __construct($pattern, $options = '', $delimiter = null) { if (null !== $delimiter) { // removes delimiter escaping $pattern = str_replace('\\'.$delimiter, $delimiter, $pattern); } $this->parsePattern($pattern); $this->options = $options; } /** * @return string */ public function __toString() { return $this->render(); } /** * {@inheritdoc} */ public function render() { return self::BOUNDARY .$this->renderPattern() .self::BOUNDARY .$this->options; } /** * {@inheritdoc} */ public function renderPattern() { return ($this->startFlag ? self::START_FLAG : '') .($this->startJoker ? self::JOKER : '') .str_replace(self::BOUNDARY, '\\'.self::BOUNDARY, $this->pattern) .($this->endJoker ? self::JOKER : '') .($this->endFlag ? self::END_FLAG : ''); } /** * {@inheritdoc} */ public function isCaseSensitive() { return !$this->hasOption('i'); } /** * {@inheritdoc} */ public function getType() { return Expression::TYPE_REGEX; } /** * {@inheritdoc} */ public function prepend($expr) { $this->pattern = $expr.$this->pattern; return $this; } /** * {@inheritdoc} */ public function append($expr) { $this->pattern .= $expr; return $this; } /** * @param string $option * * @return bool */ public function hasOption($option) { return false !== strpos($this->options, $option); } /** * @param string $option * * @return Regex */ public function addOption($option) { if (!$this->hasOption($option)) { $this->options .= $option; } return $this; } /** * @param string $option * * @return Regex */ public function removeOption($option) { $this->options = str_replace($option, '', $this->options); return $this; } /** * @param bool $startFlag * * @return Regex */ public function setStartFlag($startFlag) { $this->startFlag = $startFlag; return $this; } /** * @return bool */ public function hasStartFlag() { return $this->startFlag; } /** * @param bool $endFlag * * @return Regex */ public function setEndFlag($endFlag) { $this->endFlag = (bool) $endFlag; return $this; } /** * @return bool */ public function hasEndFlag() { return $this->endFlag; } /** * @param bool $startJoker * * @return Regex */ public function setStartJoker($startJoker) { $this->startJoker = $startJoker; return $this; } /** * @return bool */ public function hasStartJoker() { return $this->startJoker; } /** * @param bool $endJoker * * @return Regex */ public function setEndJoker($endJoker) { $this->endJoker = (bool) $endJoker; return $this; } /** * @return bool */ public function hasEndJoker() { return $this->endJoker; } /** * @param array $replacement * * @return Regex */ public function replaceJokers($replacement) { $replace = function ($subject) use ($replacement) { $subject = $subject[0]; $replace = 0 === substr_count($subject, '\\') % 2; return $replace ? str_replace('.', $replacement, $subject) : $subject; }; $this->pattern = preg_replace_callback('~[\\\\]*\\.~', $replace, $this->pattern); return $this; } /** * @param string $pattern */ private function parsePattern($pattern) { if ($this->startFlag = self::START_FLAG === substr($pattern, 0, 1)) { $pattern = substr($pattern, 1); } if ($this->startJoker = self::JOKER === substr($pattern, 0, 2)) { $pattern = substr($pattern, 2); } if ($this->endFlag = (self::END_FLAG === substr($pattern, -1) && self::ESCAPING !== substr($pattern, -2, -1))) { $pattern = substr($pattern, 0, -1); } if ($this->endJoker = (self::JOKER === substr($pattern, -2) && self::ESCAPING !== substr($pattern, -3, -2))) { $pattern = substr($pattern, 0, -2); } $this->pattern = $pattern; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Expression; @trigger_error('The '.__NAMESPACE__.'\ValueInterface interface is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); /** * @author Jean-François Simon */ interface ValueInterface { /** * Renders string representation of expression. * * @return string */ public function render(); /** * Renders string representation of pattern. * * @return string */ public function renderPattern(); /** * Returns value case sensitivity. * * @return bool */ public function isCaseSensitive(); /** * Returns expression type. * * @return int */ public function getType(); /** * @param string $expr * * @return ValueInterface */ public function prepend($expr); /** * @param string $expr * * @return ValueInterface */ public function append($expr); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder; use Symfony\Component\Finder\Adapter\AdapterInterface; use Symfony\Component\Finder\Adapter\GnuFindAdapter; use Symfony\Component\Finder\Adapter\BsdFindAdapter; use Symfony\Component\Finder\Adapter\PhpAdapter; use Symfony\Component\Finder\Comparator\DateComparator; use Symfony\Component\Finder\Comparator\NumberComparator; use Symfony\Component\Finder\Exception\ExceptionInterface; use Symfony\Component\Finder\Iterator\CustomFilterIterator; use Symfony\Component\Finder\Iterator\DateRangeFilterIterator; use Symfony\Component\Finder\Iterator\DepthRangeFilterIterator; use Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator; use Symfony\Component\Finder\Iterator\FilecontentFilterIterator; use Symfony\Component\Finder\Iterator\FilenameFilterIterator; use Symfony\Component\Finder\Iterator\SizeRangeFilterIterator; use Symfony\Component\Finder\Iterator\SortableIterator; /** * Finder allows to build rules to find files and directories. * * It is a thin wrapper around several specialized iterator classes. * * All rules may be invoked several times. * * All methods return the current Finder object to allow easy chaining: * * $finder = Finder::create()->files()->name('*.php')->in(__DIR__); * * @author Fabien Potencier */ class Finder implements \IteratorAggregate, \Countable { const IGNORE_VCS_FILES = 1; const IGNORE_DOT_FILES = 2; private $mode = 0; private $names = array(); private $notNames = array(); private $exclude = array(); private $filters = array(); private $depths = array(); private $sizes = array(); private $followLinks = false; private $sort = false; private $ignore = 0; private $dirs = array(); private $dates = array(); private $iterators = array(); private $contains = array(); private $notContains = array(); private $adapters = null; private $paths = array(); private $notPaths = array(); private $ignoreUnreadableDirs = false; private static $vcsPatterns = array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'); /** * Constructor. */ public function __construct() { $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES; } /** * Creates a new Finder. * * @return Finder A new Finder instance */ public static function create() { return new static(); } /** * Registers a finder engine implementation. * * @param AdapterInterface $adapter An adapter instance * @param int $priority Highest is selected first * * @return Finder The current Finder instance * * @deprecated since 2.8, to be removed in 3.0. */ public function addAdapter(AdapterInterface $adapter, $priority = 0) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); $this->initDefaultAdapters(); $this->adapters[$adapter->getName()] = array( 'adapter' => $adapter, 'priority' => $priority, 'selected' => false, ); return $this->sortAdapters(); } /** * Sets the selected adapter to the best one according to the current platform the code is run on. * * @return Finder The current Finder instance * * @deprecated since 2.8, to be removed in 3.0. */ public function useBestAdapter() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); $this->initDefaultAdapters(); $this->resetAdapterSelection(); return $this->sortAdapters(); } /** * Selects the adapter to use. * * @param string $name * * @return Finder The current Finder instance * * @throws \InvalidArgumentException * * @deprecated since 2.8, to be removed in 3.0. */ public function setAdapter($name) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); $this->initDefaultAdapters(); if (!isset($this->adapters[$name])) { throw new \InvalidArgumentException(sprintf('Adapter "%s" does not exist.', $name)); } $this->resetAdapterSelection(); $this->adapters[$name]['selected'] = true; return $this->sortAdapters(); } /** * Removes all adapters registered in the finder. * * @return Finder The current Finder instance * * @deprecated since 2.8, to be removed in 3.0. */ public function removeAdapters() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); $this->adapters = array(); return $this; } /** * Returns registered adapters ordered by priority without extra information. * * @return AdapterInterface[] * * @deprecated since 2.8, to be removed in 3.0. */ public function getAdapters() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); $this->initDefaultAdapters(); return array_values(array_map(function (array $adapter) { return $adapter['adapter']; }, $this->adapters)); } /** * Restricts the matching to directories only. * * @return Finder|SplFileInfo[] The current Finder instance */ public function directories() { $this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES; return $this; } /** * Restricts the matching to files only. * * @return Finder|SplFileInfo[] The current Finder instance */ public function files() { $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES; return $this; } /** * Adds tests for the directory depth. * * Usage: * * $finder->depth('> 1') // the Finder will start matching at level 1. * $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point. * * @param string|int $level The depth level expression * * @return Finder|SplFileInfo[] The current Finder instance * * @see DepthRangeFilterIterator * @see NumberComparator */ public function depth($level) { $this->depths[] = new Comparator\NumberComparator($level); return $this; } /** * Adds tests for file dates (last modified). * * The date must be something that strtotime() is able to parse: * * $finder->date('since yesterday'); * $finder->date('until 2 days ago'); * $finder->date('> now - 2 hours'); * $finder->date('>= 2005-10-15'); * * @param string $date A date range string * * @return Finder|SplFileInfo[] The current Finder instance * * @see strtotime * @see DateRangeFilterIterator * @see DateComparator */ public function date($date) { $this->dates[] = new Comparator\DateComparator($date); return $this; } /** * Adds rules that files must match. * * You can use patterns (delimited with / sign), globs or simple strings. * * $finder->name('*.php') * $finder->name('/\.php$/') // same as above * $finder->name('test.php') * * @param string $pattern A pattern (a regexp, a glob, or a string) * * @return Finder|SplFileInfo[] The current Finder instance * * @see FilenameFilterIterator */ public function name($pattern) { $this->names[] = $pattern; return $this; } /** * Adds rules that files must not match. * * @param string $pattern A pattern (a regexp, a glob, or a string) * * @return Finder|SplFileInfo[] The current Finder instance * * @see FilenameFilterIterator */ public function notName($pattern) { $this->notNames[] = $pattern; return $this; } /** * Adds tests that file contents must match. * * Strings or PCRE patterns can be used: * * $finder->contains('Lorem ipsum') * $finder->contains('/Lorem ipsum/i') * * @param string $pattern A pattern (string or regexp) * * @return Finder|SplFileInfo[] The current Finder instance * * @see FilecontentFilterIterator */ public function contains($pattern) { $this->contains[] = $pattern; return $this; } /** * Adds tests that file contents must not match. * * Strings or PCRE patterns can be used: * * $finder->notContains('Lorem ipsum') * $finder->notContains('/Lorem ipsum/i') * * @param string $pattern A pattern (string or regexp) * * @return Finder|SplFileInfo[] The current Finder instance * * @see FilecontentFilterIterator */ public function notContains($pattern) { $this->notContains[] = $pattern; return $this; } /** * Adds rules that filenames must match. * * You can use patterns (delimited with / sign) or simple strings. * * $finder->path('some/special/dir') * $finder->path('/some\/special\/dir/') // same as above * * Use only / as dirname separator. * * @param string $pattern A pattern (a regexp or a string) * * @return Finder|SplFileInfo[] The current Finder instance * * @see FilenameFilterIterator */ public function path($pattern) { $this->paths[] = $pattern; return $this; } /** * Adds rules that filenames must not match. * * You can use patterns (delimited with / sign) or simple strings. * * $finder->notPath('some/special/dir') * $finder->notPath('/some\/special\/dir/') // same as above * * Use only / as dirname separator. * * @param string $pattern A pattern (a regexp or a string) * * @return Finder|SplFileInfo[] The current Finder instance * * @see FilenameFilterIterator */ public function notPath($pattern) { $this->notPaths[] = $pattern; return $this; } /** * Adds tests for file sizes. * * $finder->size('> 10K'); * $finder->size('<= 1Ki'); * $finder->size(4); * * @param string|int $size A size range string or an integer * * @return Finder|SplFileInfo[] The current Finder instance * * @see SizeRangeFilterIterator * @see NumberComparator */ public function size($size) { $this->sizes[] = new Comparator\NumberComparator($size); return $this; } /** * Excludes directories. * * @param string|array $dirs A directory path or an array of directories * * @return Finder|SplFileInfo[] The current Finder instance * * @see ExcludeDirectoryFilterIterator */ public function exclude($dirs) { $this->exclude = array_merge($this->exclude, (array) $dirs); return $this; } /** * Excludes "hidden" directories and files (starting with a dot). * * @param bool $ignoreDotFiles Whether to exclude "hidden" files or not * * @return Finder|SplFileInfo[] The current Finder instance * * @see ExcludeDirectoryFilterIterator */ public function ignoreDotFiles($ignoreDotFiles) { if ($ignoreDotFiles) { $this->ignore |= static::IGNORE_DOT_FILES; } else { $this->ignore &= ~static::IGNORE_DOT_FILES; } return $this; } /** * Forces the finder to ignore version control directories. * * @param bool $ignoreVCS Whether to exclude VCS files or not * * @return Finder|SplFileInfo[] The current Finder instance * * @see ExcludeDirectoryFilterIterator */ public function ignoreVCS($ignoreVCS) { if ($ignoreVCS) { $this->ignore |= static::IGNORE_VCS_FILES; } else { $this->ignore &= ~static::IGNORE_VCS_FILES; } return $this; } /** * Adds VCS patterns. * * @see ignoreVCS() * * @param string|string[] $pattern VCS patterns to ignore */ public static function addVCSPattern($pattern) { foreach ((array) $pattern as $p) { self::$vcsPatterns[] = $p; } self::$vcsPatterns = array_unique(self::$vcsPatterns); } /** * Sorts files and directories by an anonymous function. * * The anonymous function receives two \SplFileInfo instances to compare. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @param \Closure $closure An anonymous function * * @return Finder|SplFileInfo[] The current Finder instance * * @see SortableIterator */ public function sort(\Closure $closure) { $this->sort = $closure; return $this; } /** * Sorts files and directories by name. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return Finder|SplFileInfo[] The current Finder instance * * @see SortableIterator */ public function sortByName() { $this->sort = Iterator\SortableIterator::SORT_BY_NAME; return $this; } /** * Sorts files and directories by type (directories before files), then by name. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return Finder|SplFileInfo[] The current Finder instance * * @see SortableIterator */ public function sortByType() { $this->sort = Iterator\SortableIterator::SORT_BY_TYPE; return $this; } /** * Sorts files and directories by the last accessed time. * * This is the time that the file was last accessed, read or written to. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return Finder|SplFileInfo[] The current Finder instance * * @see SortableIterator */ public function sortByAccessedTime() { $this->sort = Iterator\SortableIterator::SORT_BY_ACCESSED_TIME; return $this; } /** * Sorts files and directories by the last inode changed time. * * This is the time that the inode information was last modified (permissions, owner, group or other metadata). * * On Windows, since inode is not available, changed time is actually the file creation time. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return Finder|SplFileInfo[] The current Finder instance * * @see SortableIterator */ public function sortByChangedTime() { $this->sort = Iterator\SortableIterator::SORT_BY_CHANGED_TIME; return $this; } /** * Sorts files and directories by the last modified time. * * This is the last time the actual contents of the file were last modified. * * This can be slow as all the matching files and directories must be retrieved for comparison. * * @return Finder|SplFileInfo[] The current Finder instance * * @see SortableIterator */ public function sortByModifiedTime() { $this->sort = Iterator\SortableIterator::SORT_BY_MODIFIED_TIME; return $this; } /** * Filters the iterator with an anonymous function. * * The anonymous function receives a \SplFileInfo and must return false * to remove files. * * @param \Closure $closure An anonymous function * * @return Finder|SplFileInfo[] The current Finder instance * * @see CustomFilterIterator */ public function filter(\Closure $closure) { $this->filters[] = $closure; return $this; } /** * Forces the following of symlinks. * * @return Finder|SplFileInfo[] The current Finder instance */ public function followLinks() { $this->followLinks = true; return $this; } /** * Tells finder to ignore unreadable directories. * * By default, scanning unreadable directories content throws an AccessDeniedException. * * @param bool $ignore * * @return Finder|SplFileInfo[] The current Finder instance */ public function ignoreUnreadableDirs($ignore = true) { $this->ignoreUnreadableDirs = (bool) $ignore; return $this; } /** * Searches files and directories which match defined rules. * * @param string|array $dirs A directory path or an array of directories * * @return Finder|SplFileInfo[] The current Finder instance * * @throws \InvalidArgumentException if one of the directories does not exist */ public function in($dirs) { $resolvedDirs = array(); foreach ((array) $dirs as $dir) { if (is_dir($dir)) { $resolvedDirs[] = $dir; } elseif ($glob = glob($dir, (defined('GLOB_BRACE') ? GLOB_BRACE : 0) | GLOB_ONLYDIR)) { $resolvedDirs = array_merge($resolvedDirs, $glob); } else { throw new \InvalidArgumentException(sprintf('The "%s" directory does not exist.', $dir)); } } $this->dirs = array_merge($this->dirs, $resolvedDirs); return $this; } /** * Returns an Iterator for the current Finder configuration. * * This method implements the IteratorAggregate interface. * * @return \Iterator|SplFileInfo[] An iterator * * @throws \LogicException if the in() method has not been called */ public function getIterator() { if (0 === count($this->dirs) && 0 === count($this->iterators)) { throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.'); } if (1 === count($this->dirs) && 0 === count($this->iterators)) { return $this->searchInDirectory($this->dirs[0]); } $iterator = new \AppendIterator(); foreach ($this->dirs as $dir) { $iterator->append($this->searchInDirectory($dir)); } foreach ($this->iterators as $it) { $iterator->append($it); } return $iterator; } /** * Appends an existing set of files/directories to the finder. * * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array. * * @param mixed $iterator * * @return Finder|SplFileInfo[] The finder * * @throws \InvalidArgumentException When the given argument is not iterable. */ public function append($iterator) { if ($iterator instanceof \IteratorAggregate) { $this->iterators[] = $iterator->getIterator(); } elseif ($iterator instanceof \Iterator) { $this->iterators[] = $iterator; } elseif ($iterator instanceof \Traversable || is_array($iterator)) { $it = new \ArrayIterator(); foreach ($iterator as $file) { $it->append($file instanceof \SplFileInfo ? $file : new \SplFileInfo($file)); } $this->iterators[] = $it; } else { throw new \InvalidArgumentException('Finder::append() method wrong argument type.'); } return $this; } /** * Counts all the results collected by the iterators. * * @return int */ public function count() { return iterator_count($this->getIterator()); } /** * @return Finder The current Finder instance */ private function sortAdapters() { uasort($this->adapters, function (array $a, array $b) { if ($a['selected'] || $b['selected']) { return $a['selected'] ? -1 : 1; } return $a['priority'] > $b['priority'] ? -1 : 1; }); return $this; } /** * @param $dir * * @return \Iterator */ private function searchInDirectory($dir) { if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) { $this->exclude = array_merge($this->exclude, self::$vcsPatterns); } if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) { $this->notPaths[] = '#(^|/)\..+(/|$)#'; } if ($this->adapters) { foreach ($this->adapters as $adapter) { if ($adapter['adapter']->isSupported()) { try { return $this ->buildAdapter($adapter['adapter']) ->searchInDirectory($dir); } catch (ExceptionInterface $e) { } } } } $minDepth = 0; $maxDepth = PHP_INT_MAX; foreach ($this->depths as $comparator) { switch ($comparator->getOperator()) { case '>': $minDepth = $comparator->getTarget() + 1; break; case '>=': $minDepth = $comparator->getTarget(); break; case '<': $maxDepth = $comparator->getTarget() - 1; break; case '<=': $maxDepth = $comparator->getTarget(); break; default: $minDepth = $maxDepth = $comparator->getTarget(); } } $flags = \RecursiveDirectoryIterator::SKIP_DOTS; if ($this->followLinks) { $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; } $iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs); if ($this->exclude) { $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude); } $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); if ($minDepth > 0 || $maxDepth < PHP_INT_MAX) { $iterator = new Iterator\DepthRangeFilterIterator($iterator, $minDepth, $maxDepth); } if ($this->mode) { $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode); } if ($this->names || $this->notNames) { $iterator = new Iterator\FilenameFilterIterator($iterator, $this->names, $this->notNames); } if ($this->contains || $this->notContains) { $iterator = new Iterator\FilecontentFilterIterator($iterator, $this->contains, $this->notContains); } if ($this->sizes) { $iterator = new Iterator\SizeRangeFilterIterator($iterator, $this->sizes); } if ($this->dates) { $iterator = new Iterator\DateRangeFilterIterator($iterator, $this->dates); } if ($this->filters) { $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters); } if ($this->paths || $this->notPaths) { $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $this->notPaths); } if ($this->sort) { $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort); $iterator = $iteratorAggregate->getIterator(); } return $iterator; } /** * @param AdapterInterface $adapter * * @return AdapterInterface */ private function buildAdapter(AdapterInterface $adapter) { return $adapter ->setFollowLinks($this->followLinks) ->setDepths($this->depths) ->setMode($this->mode) ->setExclude($this->exclude) ->setNames($this->names) ->setNotNames($this->notNames) ->setContains($this->contains) ->setNotContains($this->notContains) ->setSizes($this->sizes) ->setDates($this->dates) ->setFilters($this->filters) ->setSort($this->sort) ->setPath($this->paths) ->setNotPath($this->notPaths) ->ignoreUnreadableDirs($this->ignoreUnreadableDirs); } /** * Unselects all adapters. */ private function resetAdapterSelection() { $this->adapters = array_map(function (array $properties) { $properties['selected'] = false; return $properties; }, $this->adapters); } private function initDefaultAdapters() { if (null === $this->adapters) { $this->adapters = array(); $this ->addAdapter(new GnuFindAdapter()) ->addAdapter(new BsdFindAdapter()) ->addAdapter(new PhpAdapter(), -50) ->setAdapter('php') ; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder; /** * Glob matches globbing patterns against text. * * if match_glob("foo.*", "foo.bar") echo "matched\n"; * * // prints foo.bar and foo.baz * $regex = glob_to_regex("foo.*"); * for (array('foo.bar', 'foo.baz', 'foo', 'bar') as $t) * { * if (/$regex/) echo "matched: $car\n"; * } * * Glob implements glob(3) style matching that can be used to match * against text, rather than fetching names from a filesystem. * * Based on the Perl Text::Glob module. * * @author Fabien Potencier PHP port * @author Richard Clamp Perl version * @copyright 2004-2005 Fabien Potencier * @copyright 2002 Richard Clamp */ class Glob { /** * Returns a regexp which is the equivalent of the glob pattern. * * @param string $glob The glob pattern * @param bool $strictLeadingDot * @param bool $strictWildcardSlash * @param string $delimiter Optional delimiter * * @return string regex The regexp */ public static function toRegex($glob, $strictLeadingDot = true, $strictWildcardSlash = true, $delimiter = '#') { $firstByte = true; $escaping = false; $inCurlies = 0; $regex = ''; $sizeGlob = strlen($glob); for ($i = 0; $i < $sizeGlob; ++$i) { $car = $glob[$i]; if ($firstByte) { if ($strictLeadingDot && '.' !== $car) { $regex .= '(?=[^\.])'; } $firstByte = false; } if ('/' === $car) { $firstByte = true; } if ($delimiter === $car || '.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) { $regex .= "\\$car"; } elseif ('*' === $car) { $regex .= $escaping ? '\\*' : ($strictWildcardSlash ? '[^/]*' : '.*'); } elseif ('?' === $car) { $regex .= $escaping ? '\\?' : ($strictWildcardSlash ? '[^/]' : '.'); } elseif ('{' === $car) { $regex .= $escaping ? '\\{' : '('; if (!$escaping) { ++$inCurlies; } } elseif ('}' === $car && $inCurlies) { $regex .= $escaping ? '}' : ')'; if (!$escaping) { --$inCurlies; } } elseif (',' === $car && $inCurlies) { $regex .= $escaping ? ',' : '|'; } elseif ('\\' === $car) { if ($escaping) { $regex .= '\\\\'; $escaping = false; } else { $escaping = true; } continue; } else { $regex .= $car; } $escaping = false; } return $delimiter.'^'.$regex.'$'.$delimiter; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * CustomFilterIterator filters files by applying anonymous functions. * * The anonymous function receives a \SplFileInfo and must return false * to remove files. * * @author Fabien Potencier */ class CustomFilterIterator extends FilterIterator { private $filters = array(); /** * Constructor. * * @param \Iterator $iterator The Iterator to filter * @param callable[] $filters An array of PHP callbacks * * @throws \InvalidArgumentException */ public function __construct(\Iterator $iterator, array $filters) { foreach ($filters as $filter) { if (!is_callable($filter)) { throw new \InvalidArgumentException('Invalid PHP callback.'); } } $this->filters = $filters; parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { $fileinfo = $this->current(); foreach ($this->filters as $filter) { if (false === call_user_func($filter, $fileinfo)) { return false; } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Comparator\DateComparator; /** * DateRangeFilterIterator filters out files that are not in the given date range (last modified dates). * * @author Fabien Potencier */ class DateRangeFilterIterator extends FilterIterator { private $comparators = array(); /** * Constructor. * * @param \Iterator $iterator The Iterator to filter * @param DateComparator[] $comparators An array of DateComparator instances */ public function __construct(\Iterator $iterator, array $comparators) { $this->comparators = $comparators; parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { $fileinfo = $this->current(); if (!file_exists($fileinfo->getPathname())) { return false; } $filedate = $fileinfo->getMTime(); foreach ($this->comparators as $compare) { if (!$compare->test($filedate)) { return false; } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * DepthRangeFilterIterator limits the directory depth. * * @author Fabien Potencier */ class DepthRangeFilterIterator extends FilterIterator { private $minDepth = 0; /** * Constructor. * * @param \RecursiveIteratorIterator $iterator The Iterator to filter * @param int $minDepth The min depth * @param int $maxDepth The max depth */ public function __construct(\RecursiveIteratorIterator $iterator, $minDepth = 0, $maxDepth = PHP_INT_MAX) { $this->minDepth = $minDepth; $iterator->setMaxDepth(PHP_INT_MAX === $maxDepth ? -1 : $maxDepth); parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { return $this->getInnerIterator()->getDepth() >= $this->minDepth; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * ExcludeDirectoryFilterIterator filters out directories. * * @author Fabien Potencier */ class ExcludeDirectoryFilterIterator extends FilterIterator implements \RecursiveIterator { private $iterator; private $isRecursive; private $excludedDirs = array(); private $excludedPattern; /** * Constructor. * * @param \Iterator $iterator The Iterator to filter * @param array $directories An array of directories to exclude */ public function __construct(\Iterator $iterator, array $directories) { $this->iterator = $iterator; $this->isRecursive = $iterator instanceof \RecursiveIterator; $patterns = array(); foreach ($directories as $directory) { $directory = rtrim($directory, '/'); if (!$this->isRecursive || false !== strpos($directory, '/')) { $patterns[] = preg_quote($directory, '#'); } else { $this->excludedDirs[$directory] = true; } } if ($patterns) { $this->excludedPattern = '#(?:^|/)(?:'.implode('|', $patterns).')(?:/|$)#'; } parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool True if the value should be kept, false otherwise */ public function accept() { if ($this->isRecursive && isset($this->excludedDirs[$this->getFilename()]) && $this->isDir()) { return false; } if ($this->excludedPattern) { $path = $this->isDir() ? $this->current()->getRelativePathname() : $this->current()->getRelativePath(); $path = str_replace('\\', '/', $path); return !preg_match($this->excludedPattern, $path); } return true; } public function hasChildren() { return $this->isRecursive && $this->iterator->hasChildren(); } public function getChildren() { $children = new self($this->iterator->getChildren(), array()); $children->excludedDirs = $this->excludedDirs; $children->excludedPattern = $this->excludedPattern; return $children; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * FilecontentFilterIterator filters files by their contents using patterns (regexps or strings). * * @author Fabien Potencier * @author Włodzimierz Gajda */ class FilecontentFilterIterator extends MultiplePcreFilterIterator { /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { if (!$this->matchRegexps && !$this->noMatchRegexps) { return true; } $fileinfo = $this->current(); if ($fileinfo->isDir() || !$fileinfo->isReadable()) { return false; } $content = $fileinfo->getContents(); if (!$content) { return false; } return $this->isAccepted($content); } /** * Converts string to regexp if necessary. * * @param string $str Pattern: string or regexp * * @return string regexp corresponding to a given string or regexp */ protected function toRegex($str) { return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Glob; /** * FilenameFilterIterator filters files by patterns (a regexp, a glob, or a string). * * @author Fabien Potencier */ class FilenameFilterIterator extends MultiplePcreFilterIterator { /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { return $this->isAccepted($this->current()->getFilename()); } /** * Converts glob to regexp. * * PCRE patterns are left unchanged. * Glob strings are transformed with Glob::toRegex(). * * @param string $str Pattern: glob or regexp * * @return string regexp corresponding to a given glob or regexp */ protected function toRegex($str) { return $this->isRegex($str) ? $str : Glob::toRegex($str); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; @trigger_error('The '.__NAMESPACE__.'\FilePathsIterator class is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); use Symfony\Component\Finder\SplFileInfo; /** * Iterate over shell command result. * * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. */ class FilePathsIterator extends \ArrayIterator { /** * @var string */ private $baseDir; /** * @var int */ private $baseDirLength; /** * @var string */ private $subPath; /** * @var string */ private $subPathname; /** * @var SplFileInfo */ private $current; /** * @param array $paths List of paths returned by shell command * @param string $baseDir Base dir for relative path building */ public function __construct(array $paths, $baseDir) { $this->baseDir = $baseDir; $this->baseDirLength = strlen($baseDir); parent::__construct($paths); } /** * @param string $name * @param array $arguments * * @return mixed */ public function __call($name, array $arguments) { return call_user_func_array(array($this->current(), $name), $arguments); } /** * Return an instance of SplFileInfo with support for relative paths. * * @return SplFileInfo File information */ public function current() { return $this->current; } /** * @return string */ public function key() { return $this->current->getPathname(); } public function next() { parent::next(); $this->buildProperties(); } public function rewind() { parent::rewind(); $this->buildProperties(); } /** * @return string */ public function getSubPath() { return $this->subPath; } /** * @return string */ public function getSubPathname() { return $this->subPathname; } private function buildProperties() { $absolutePath = parent::current(); if ($this->baseDir === substr($absolutePath, 0, $this->baseDirLength)) { $this->subPathname = ltrim(substr($absolutePath, $this->baseDirLength), '/\\'); $dir = dirname($this->subPathname); $this->subPath = '.' === $dir ? '' : $dir; } else { $this->subPath = $this->subPathname = ''; } $this->current = new SplFileInfo(parent::current(), $this->subPath, $this->subPathname); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * FileTypeFilterIterator only keeps files, directories, or both. * * @author Fabien Potencier */ class FileTypeFilterIterator extends FilterIterator { const ONLY_FILES = 1; const ONLY_DIRECTORIES = 2; private $mode; /** * Constructor. * * @param \Iterator $iterator The Iterator to filter * @param int $mode The mode (self::ONLY_FILES or self::ONLY_DIRECTORIES) */ public function __construct(\Iterator $iterator, $mode) { $this->mode = $mode; parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { $fileinfo = $this->current(); if (self::ONLY_DIRECTORIES === (self::ONLY_DIRECTORIES & $this->mode) && $fileinfo->isFile()) { return false; } elseif (self::ONLY_FILES === (self::ONLY_FILES & $this->mode) && $fileinfo->isDir()) { return false; } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * This iterator just overrides the rewind method in order to correct a PHP bug, * which existed before version 5.5.23/5.6.7. * * @see https://bugs.php.net/68557 * * @author Alex Bogomazov */ abstract class FilterIterator extends \FilterIterator { /** * This is a workaround for the problem with \FilterIterator leaving inner \FilesystemIterator in wrong state after * rewind in some cases. * * @see FilterIterator::rewind() */ public function rewind() { if (PHP_VERSION_ID > 50607 || (PHP_VERSION_ID > 50523 && PHP_VERSION_ID < 50600)) { parent::rewind(); return; } $iterator = $this; while ($iterator instanceof \OuterIterator) { $innerIterator = $iterator->getInnerIterator(); if ($innerIterator instanceof RecursiveDirectoryIterator) { // this condition is necessary for iterators to work properly with non-local filesystems like ftp if ($innerIterator->isRewindable()) { $innerIterator->next(); $innerIterator->rewind(); } } elseif ($innerIterator instanceof \FilesystemIterator) { $innerIterator->next(); $innerIterator->rewind(); } $iterator = $innerIterator; } parent::rewind(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * MultiplePcreFilterIterator filters files using patterns (regexps, globs or strings). * * @author Fabien Potencier */ abstract class MultiplePcreFilterIterator extends FilterIterator { protected $matchRegexps = array(); protected $noMatchRegexps = array(); /** * Constructor. * * @param \Iterator $iterator The Iterator to filter * @param array $matchPatterns An array of patterns that need to match * @param array $noMatchPatterns An array of patterns that need to not match */ public function __construct(\Iterator $iterator, array $matchPatterns, array $noMatchPatterns) { foreach ($matchPatterns as $pattern) { $this->matchRegexps[] = $this->toRegex($pattern); } foreach ($noMatchPatterns as $pattern) { $this->noMatchRegexps[] = $this->toRegex($pattern); } parent::__construct($iterator); } /** * Checks whether the string is accepted by the regex filters. * * If there is no regexps defined in the class, this method will accept the string. * Such case can be handled by child classes before calling the method if they want to * apply a different behavior. * * @param string $string The string to be matched against filters * * @return bool */ protected function isAccepted($string) { // should at least not match one rule to exclude foreach ($this->noMatchRegexps as $regex) { if (preg_match($regex, $string)) { return false; } } // should at least match one rule if ($this->matchRegexps) { foreach ($this->matchRegexps as $regex) { if (preg_match($regex, $string)) { return true; } } return false; } // If there is no match rules, the file is accepted return true; } /** * Checks whether the string is a regex. * * @param string $str * * @return bool Whether the given string is a regex */ protected function isRegex($str) { if (preg_match('/^(.{3,}?)[imsxuADU]*$/', $str, $m)) { $start = substr($m[1], 0, 1); $end = substr($m[1], -1); if ($start === $end) { return !preg_match('/[*?[:alnum:] \\\\]/', $start); } foreach (array(array('{', '}'), array('(', ')'), array('[', ']'), array('<', '>')) as $delimiters) { if ($start === $delimiters[0] && $end === $delimiters[1]) { return true; } } } return false; } /** * Converts string into regexp. * * @param string $str Pattern * * @return string regexp corresponding to a given string */ abstract protected function toRegex($str); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * PathFilterIterator filters files by path patterns (e.g. some/special/dir). * * @author Fabien Potencier * @author Włodzimierz Gajda */ class PathFilterIterator extends MultiplePcreFilterIterator { /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { $filename = $this->current()->getRelativePathname(); if ('\\' === DIRECTORY_SEPARATOR) { $filename = str_replace('\\', '/', $filename); } return $this->isAccepted($filename); } /** * Converts strings to regexp. * * PCRE patterns are left unchanged. * * Default conversion: * 'lorem/ipsum/dolor' ==> 'lorem\/ipsum\/dolor/' * * Use only / as directory separator (on Windows also). * * @param string $str Pattern: regexp or dirname * * @return string regexp corresponding to a given string or regexp */ protected function toRegex($str) { return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Exception\AccessDeniedException; use Symfony\Component\Finder\SplFileInfo; /** * Extends the \RecursiveDirectoryIterator to support relative paths. * * @author Victor Berchet */ class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator { /** * @var bool */ private $ignoreUnreadableDirs; /** * @var bool */ private $rewindable; // these 3 properties take part of the performance optimization to avoid redoing the same work in all iterations private $rootPath; private $subPath; private $directorySeparator = '/'; /** * Constructor. * * @param string $path * @param int $flags * @param bool $ignoreUnreadableDirs * * @throws \RuntimeException */ public function __construct($path, $flags, $ignoreUnreadableDirs = false) { if ($flags & (self::CURRENT_AS_PATHNAME | self::CURRENT_AS_SELF)) { throw new \RuntimeException('This iterator only support returning current as fileinfo.'); } parent::__construct($path, $flags); $this->ignoreUnreadableDirs = $ignoreUnreadableDirs; $this->rootPath = (string) $path; if ('/' !== DIRECTORY_SEPARATOR && !($flags & self::UNIX_PATHS)) { $this->directorySeparator = DIRECTORY_SEPARATOR; } } /** * Return an instance of SplFileInfo with support for relative paths. * * @return SplFileInfo File information */ public function current() { // the logic here avoids redoing the same work in all iterations if (null === $subPathname = $this->subPath) { $subPathname = $this->subPath = (string) $this->getSubPath(); } if ('' !== $subPathname) { $subPathname .= $this->directorySeparator; } $subPathname .= $this->getFilename(); return new SplFileInfo($this->rootPath.$this->directorySeparator.$subPathname, $this->subPath, $subPathname); } /** * @return \RecursiveIterator * * @throws AccessDeniedException */ public function getChildren() { try { $children = parent::getChildren(); if ($children instanceof self) { // parent method will call the constructor with default arguments, so unreadable dirs won't be ignored anymore $children->ignoreUnreadableDirs = $this->ignoreUnreadableDirs; // performance optimization to avoid redoing the same work in all children $children->rewindable = &$this->rewindable; $children->rootPath = $this->rootPath; } return $children; } catch (\UnexpectedValueException $e) { if ($this->ignoreUnreadableDirs) { // If directory is unreadable and finder is set to ignore it, a fake empty content is returned. return new \RecursiveArrayIterator(array()); } else { throw new AccessDeniedException($e->getMessage(), $e->getCode(), $e); } } } /** * Do nothing for non rewindable stream. */ public function rewind() { if (false === $this->isRewindable()) { return; } // @see https://bugs.php.net/68557 if (PHP_VERSION_ID < 50523 || PHP_VERSION_ID >= 50600 && PHP_VERSION_ID < 50607) { parent::next(); } parent::rewind(); } /** * Checks if the stream is rewindable. * * @return bool true when the stream is rewindable, false otherwise */ public function isRewindable() { if (null !== $this->rewindable) { return $this->rewindable; } // workaround for an HHVM bug, should be removed when https://github.com/facebook/hhvm/issues/7281 is fixed if ('' === $this->getPath()) { return $this->rewindable = false; } if (false !== $stream = @opendir($this->getPath())) { $infos = stream_get_meta_data($stream); closedir($stream); if ($infos['seekable']) { return $this->rewindable = true; } } return $this->rewindable = false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; use Symfony\Component\Finder\Comparator\NumberComparator; /** * SizeRangeFilterIterator filters out files that are not in the given size range. * * @author Fabien Potencier */ class SizeRangeFilterIterator extends FilterIterator { private $comparators = array(); /** * Constructor. * * @param \Iterator $iterator The Iterator to filter * @param NumberComparator[] $comparators An array of NumberComparator instances */ public function __construct(\Iterator $iterator, array $comparators) { $this->comparators = $comparators; parent::__construct($iterator); } /** * Filters the iterator values. * * @return bool true if the value should be kept, false otherwise */ public function accept() { $fileinfo = $this->current(); if (!$fileinfo->isFile()) { return true; } $filesize = $fileinfo->getSize(); foreach ($this->comparators as $compare) { if (!$compare->test($filesize)) { return false; } } return true; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Iterator; /** * SortableIterator applies a sort on a given Iterator. * * @author Fabien Potencier */ class SortableIterator implements \IteratorAggregate { const SORT_BY_NAME = 1; const SORT_BY_TYPE = 2; const SORT_BY_ACCESSED_TIME = 3; const SORT_BY_CHANGED_TIME = 4; const SORT_BY_MODIFIED_TIME = 5; private $iterator; private $sort; /** * Constructor. * * @param \Traversable $iterator The Iterator to filter * @param int|callable $sort The sort type (SORT_BY_NAME, SORT_BY_TYPE, or a PHP callback) * * @throws \InvalidArgumentException */ public function __construct(\Traversable $iterator, $sort) { $this->iterator = $iterator; if (self::SORT_BY_NAME === $sort) { $this->sort = function ($a, $b) { return strcmp($a->getRealpath() ?: $a->getPathname(), $b->getRealpath() ?: $b->getPathname()); }; } elseif (self::SORT_BY_TYPE === $sort) { $this->sort = function ($a, $b) { if ($a->isDir() && $b->isFile()) { return -1; } elseif ($a->isFile() && $b->isDir()) { return 1; } return strcmp($a->getRealpath() ?: $a->getPathname(), $b->getRealpath() ?: $b->getPathname()); }; } elseif (self::SORT_BY_ACCESSED_TIME === $sort) { $this->sort = function ($a, $b) { return $a->getATime() - $b->getATime(); }; } elseif (self::SORT_BY_CHANGED_TIME === $sort) { $this->sort = function ($a, $b) { return $a->getCTime() - $b->getCTime(); }; } elseif (self::SORT_BY_MODIFIED_TIME === $sort) { $this->sort = function ($a, $b) { return $a->getMTime() - $b->getMTime(); }; } elseif (is_callable($sort)) { $this->sort = $sort; } else { throw new \InvalidArgumentException('The SortableIterator takes a PHP callable or a valid built-in sort algorithm as an argument.'); } } public function getIterator() { $array = iterator_to_array($this->iterator, true); uasort($array, $this->sort); return new \ArrayIterator($array); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Shell; @trigger_error('The '.__NAMESPACE__.'\Command class is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); /** * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. */ class Command { /** * @var Command|null */ private $parent; /** * @var array */ private $bits = array(); /** * @var array */ private $labels = array(); /** * @var \Closure|null */ private $errorHandler; /** * Constructor. * * @param Command|null $parent Parent command */ public function __construct(Command $parent = null) { $this->parent = $parent; } /** * Returns command as string. * * @return string */ public function __toString() { return $this->join(); } /** * Creates a new Command instance. * * @param Command|null $parent Parent command * * @return Command New Command instance */ public static function create(Command $parent = null) { return new self($parent); } /** * Escapes special chars from input. * * @param string $input A string to escape * * @return string The escaped string */ public static function escape($input) { return escapeshellcmd($input); } /** * Quotes input. * * @param string $input An argument string * * @return string The quoted string */ public static function quote($input) { return escapeshellarg($input); } /** * Appends a string or a Command instance. * * @param string|Command $bit * * @return Command The current Command instance */ public function add($bit) { $this->bits[] = $bit; return $this; } /** * Prepends a string or a command instance. * * @param string|Command $bit * * @return Command The current Command instance */ public function top($bit) { array_unshift($this->bits, $bit); foreach ($this->labels as $label => $index) { $this->labels[$label] += 1; } return $this; } /** * Appends an argument, will be quoted. * * @param string $arg * * @return Command The current Command instance */ public function arg($arg) { $this->bits[] = self::quote($arg); return $this; } /** * Appends escaped special command chars. * * @param string $esc * * @return Command The current Command instance */ public function cmd($esc) { $this->bits[] = self::escape($esc); return $this; } /** * Inserts a labeled command to feed later. * * @param string $label The unique label * * @return Command The current Command instance * * @throws \RuntimeException If label already exists */ public function ins($label) { if (isset($this->labels[$label])) { throw new \RuntimeException(sprintf('Label "%s" already exists.', $label)); } $this->bits[] = self::create($this); $this->labels[$label] = count($this->bits) - 1; return $this->bits[$this->labels[$label]]; } /** * Retrieves a previously labeled command. * * @param string $label * * @return Command The labeled command * * @throws \RuntimeException */ public function get($label) { if (!isset($this->labels[$label])) { throw new \RuntimeException(sprintf('Label "%s" does not exist.', $label)); } return $this->bits[$this->labels[$label]]; } /** * Returns parent command (if any). * * @return Command Parent command * * @throws \RuntimeException If command has no parent */ public function end() { if (null === $this->parent) { throw new \RuntimeException('Calling end on root command doesn\'t make sense.'); } return $this->parent; } /** * Counts bits stored in command. * * @return int The bits count */ public function length() { return count($this->bits); } /** * @param \Closure $errorHandler * * @return Command */ public function setErrorHandler(\Closure $errorHandler) { $this->errorHandler = $errorHandler; return $this; } /** * @return \Closure|null */ public function getErrorHandler() { return $this->errorHandler; } /** * Executes current command. * * @return array The command result * * @throws \RuntimeException */ public function execute() { if (null === $errorHandler = $this->errorHandler) { exec($this->join(), $output); } else { $process = proc_open($this->join(), array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $pipes); $output = preg_split('~(\r\n|\r|\n)~', stream_get_contents($pipes[1]), -1, PREG_SPLIT_NO_EMPTY); if ($error = stream_get_contents($pipes[2])) { $errorHandler($error); } proc_close($process); } return $output ?: array(); } /** * Joins bits. * * @return string */ public function join() { return implode(' ', array_filter( array_map(function ($bit) { return $bit instanceof Command ? $bit->join() : ($bit ?: null); }, $this->bits), function ($bit) { return null !== $bit; } )); } /** * Insert a string or a Command instance before the bit at given position $index (index starts from 0). * * @param string|Command $bit * @param int $index * * @return Command The current Command instance */ public function addAtIndex($bit, $index) { array_splice($this->bits, $index, 0, $bit instanceof self ? array($bit) : $bit); return $this; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder\Shell; @trigger_error('The '.__NAMESPACE__.'\Shell class is deprecated since version 2.8 and will be removed in 3.0.', E_USER_DEPRECATED); /** * @author Jean-François Simon * * @deprecated since 2.8, to be removed in 3.0. */ class Shell { const TYPE_UNIX = 1; const TYPE_DARWIN = 2; const TYPE_CYGWIN = 3; const TYPE_WINDOWS = 4; const TYPE_BSD = 5; /** * @var string|null */ private $type; /** * Returns guessed OS type. * * @return int */ public function getType() { if (null === $this->type) { $this->type = $this->guessType(); } return $this->type; } /** * Tests if a command is available. * * @param string $command * * @return bool */ public function testCommand($command) { if (!function_exists('exec')) { return false; } // todo: find a better way (command could not be available) $testCommand = 'which '; if (self::TYPE_WINDOWS === $this->type) { $testCommand = 'where '; } $command = escapeshellcmd($command); exec($testCommand.$command, $output, $code); return 0 === $code && count($output) > 0; } /** * Guesses OS type. * * @return int */ private function guessType() { $os = strtolower(PHP_OS); if (false !== strpos($os, 'cygwin')) { return self::TYPE_CYGWIN; } if (false !== strpos($os, 'darwin')) { return self::TYPE_DARWIN; } if (false !== strpos($os, 'bsd')) { return self::TYPE_BSD; } if (0 === strpos($os, 'win')) { return self::TYPE_WINDOWS; } return self::TYPE_UNIX; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Finder; /** * Extends \SplFileInfo to support relative paths. * * @author Fabien Potencier */ class SplFileInfo extends \SplFileInfo { private $relativePath; private $relativePathname; /** * Constructor. * * @param string $file The file name * @param string $relativePath The relative path * @param string $relativePathname The relative path name */ public function __construct($file, $relativePath, $relativePathname) { parent::__construct($file); $this->relativePath = $relativePath; $this->relativePathname = $relativePathname; } /** * Returns the relative path. * * This path does not contain the file name. * * @return string the relative path */ public function getRelativePath() { return $this->relativePath; } /** * Returns the relative path name. * * This path contains the file name. * * @return string the relative path name */ public function getRelativePathname() { return $this->relativePathname; } /** * Returns the contents of the file. * * @return string the contents of the file * * @throws \RuntimeException */ public function getContents() { $level = error_reporting(0); $content = file_get_contents($this->getPathname()); error_reporting($level); if (false === $content) { $error = error_get_last(); throw new \RuntimeException($error['message']); } return $content; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Polyfill\Mbstring as p; if (!function_exists('mb_strlen')) { define('MB_CASE_UPPER', 0); define('MB_CASE_LOWER', 1); define('MB_CASE_TITLE', 2); function mb_convert_encoding($s, $to, $from = null) { return p\Mbstring::mb_convert_encoding($s, $to, $from); } function mb_decode_mimeheader($s) { return p\Mbstring::mb_decode_mimeheader($s); } function mb_encode_mimeheader($s, $charset = null, $transferEnc = null, $lf = null, $indent = null) { return p\Mbstring::mb_encode_mimeheader($s, $charset, $transferEnc, $lf, $indent); } function mb_convert_case($s, $mode, $enc = null) { return p\Mbstring::mb_convert_case($s, $mode, $enc); } function mb_internal_encoding($enc = null) { return p\Mbstring::mb_internal_encoding($enc); } function mb_language($lang = null) { return p\Mbstring::mb_language($lang); } function mb_list_encodings() { return p\Mbstring::mb_list_encodings(); } function mb_encoding_aliases($encoding) { return p\Mbstring::mb_encoding_aliases($encoding); } function mb_check_encoding($var = null, $encoding = null) { return p\Mbstring::mb_check_encoding($var, $encoding); } function mb_detect_encoding($str, $encodingList = null, $strict = false) { return p\Mbstring::mb_detect_encoding($str, $encodingList, $strict); } function mb_detect_order($encodingList = null) { return p\Mbstring::mb_detect_order($encodingList); } function mb_parse_str($s, &$result = array()) { parse_str($s, $result); } function mb_strlen($s, $enc = null) { return p\Mbstring::mb_strlen($s, $enc); } function mb_strpos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strpos($s, $needle, $offset, $enc); } function mb_strtolower($s, $enc = null) { return p\Mbstring::mb_strtolower($s, $enc); } function mb_strtoupper($s, $enc = null) { return p\Mbstring::mb_strtoupper($s, $enc); } function mb_substitute_character($char = null) { return p\Mbstring::mb_substitute_character($char); } function mb_substr($s, $start, $length = 2147483647, $enc = null) { return p\Mbstring::mb_substr($s, $start, $length, $enc); } function mb_stripos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_stripos($s, $needle, $offset, $enc); } function mb_stristr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_stristr($s, $needle, $part, $enc); } function mb_strrchr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strrchr($s, $needle, $part, $enc); } function mb_strrichr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strrichr($s, $needle, $part, $enc); } function mb_strripos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strripos($s, $needle, $offset, $enc); } function mb_strrpos($s, $needle, $offset = 0, $enc = null) { return p\Mbstring::mb_strrpos($s, $needle, $offset, $enc); } function mb_strstr($s, $needle, $part = false, $enc = null) { return p\Mbstring::mb_strstr($s, $needle, $part, $enc); } function mb_get_info($type = 'all') { return p\Mbstring::mb_get_info($type); } function mb_http_output($enc = null) { return p\Mbstring::mb_http_output($enc); } function mb_strwidth($s, $enc = null) { return p\Mbstring::mb_strwidth($s, $enc); } function mb_substr_count($haystack, $needle, $enc = null) { return p\Mbstring::mb_substr_count($haystack, $needle, $enc); } function mb_output_handler($contents, $status) { return p\Mbstring::mb_output_handler($contents, $status); } function mb_http_input($type = '') { return p\Mbstring::mb_http_input($type); } function mb_convert_variables($toEncoding, $fromEncoding, &$a = null, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null) { return p\Mbstring::mb_convert_variables($toEncoding, $fromEncoding, $a, $b, $c, $d, $e, $f); } } if (!function_exists('mb_chr')) { function mb_ord($s, $enc = null) { return p\Mbstring::mb_ord($s, $enc); } function mb_chr($code, $enc = null) { return p\Mbstring::mb_chr($code, $enc); } function mb_scrub($s, $enc = null) { $enc = null === $enc ? mb_internal_encoding() : $enc; return mb_convert_encoding($s, $enc, $enc); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Polyfill\Mbstring; /** * Partial mbstring implementation in PHP, iconv based, UTF-8 centric. * * Implemented: * - mb_chr - Returns a specific character from its Unicode code point * - mb_convert_encoding - Convert character encoding * - mb_convert_variables - Convert character code in variable(s) * - mb_decode_mimeheader - Decode string in MIME header field * - mb_encode_mimeheader - Encode string for MIME header XXX NATIVE IMPLEMENTATION IS REALLY BUGGED * - mb_convert_case - Perform case folding on a string * - mb_get_info - Get internal settings of mbstring * - mb_http_input - Detect HTTP input character encoding * - mb_http_output - Set/Get HTTP output character encoding * - mb_internal_encoding - Set/Get internal character encoding * - mb_list_encodings - Returns an array of all supported encodings * - mb_ord - Returns the Unicode code point of a character * - mb_output_handler - Callback function converts character encoding in output buffer * - mb_scrub - Replaces ill-formed byte sequences with substitute characters * - mb_strlen - Get string length * - mb_strpos - Find position of first occurrence of string in a string * - mb_strrpos - Find position of last occurrence of a string in a string * - mb_strtolower - Make a string lowercase * - mb_strtoupper - Make a string uppercase * - mb_substitute_character - Set/Get substitution character * - mb_substr - Get part of string * - mb_stripos - Finds position of first occurrence of a string within another, case insensitive * - mb_stristr - Finds first occurrence of a string within another, case insensitive * - mb_strrchr - Finds the last occurrence of a character in a string within another * - mb_strrichr - Finds the last occurrence of a character in a string within another, case insensitive * - mb_strripos - Finds position of last occurrence of a string within another, case insensitive * - mb_strstr - Finds first occurrence of a string within anothers * - mb_strwidth - Return width of string * - mb_substr_count - Count the number of substring occurrences * * Not implemented: * - mb_convert_kana - Convert "kana" one from another ("zen-kaku", "han-kaku" and more) * - mb_decode_numericentity - Decode HTML numeric string reference to character * - mb_encode_numericentity - Encode character to HTML numeric string reference * - mb_ereg_* - Regular expression with multibyte support * - mb_parse_str - Parse GET/POST/COOKIE data and set global variable * - mb_preferred_mime_name - Get MIME charset string * - mb_regex_encoding - Returns current encoding for multibyte regex as string * - mb_regex_set_options - Set/Get the default options for mbregex functions * - mb_send_mail - Send encoded mail * - mb_split - Split multibyte string using regular expression * - mb_strcut - Get part of string * - mb_strimwidth - Get truncated string with specified width * * @author Nicolas Grekas * * @internal */ final class Mbstring { const MB_CASE_FOLD = PHP_INT_MAX; private static $encodingList = array('ASCII', 'UTF-8'); private static $language = 'neutral'; private static $internalEncoding = 'UTF-8'; private static $caseFold = array( array('µ','ſ',"\xCD\x85",'ς',"\xCF\x90","\xCF\x91","\xCF\x95","\xCF\x96","\xCF\xB0","\xCF\xB1","\xCF\xB5","\xE1\xBA\x9B","\xE1\xBE\xBE"), array('μ','s','ι', 'σ','β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1",'ι'), ); public static function mb_convert_encoding($s, $toEncoding, $fromEncoding = null) { if (is_array($fromEncoding) || false !== strpos($fromEncoding, ',')) { $fromEncoding = self::mb_detect_encoding($s, $fromEncoding); } else { $fromEncoding = self::getEncoding($fromEncoding); } $toEncoding = self::getEncoding($toEncoding); if ('BASE64' === $fromEncoding) { $s = base64_decode($s); $fromEncoding = $toEncoding; } if ('BASE64' === $toEncoding) { return base64_encode($s); } if ('HTML-ENTITIES' === $toEncoding || 'HTML' === $toEncoding) { if ('HTML-ENTITIES' === $fromEncoding || 'HTML' === $fromEncoding) { $fromEncoding = 'Windows-1252'; } if ('UTF-8' !== $fromEncoding) { $s = iconv($fromEncoding, 'UTF-8//IGNORE', $s); } return preg_replace_callback('/[\x80-\xFF]+/', array(__CLASS__, 'html_encoding_callback'), $s); } if ('HTML-ENTITIES' === $fromEncoding) { $s = html_entity_decode($s, ENT_COMPAT, 'UTF-8'); $fromEncoding = 'UTF-8'; } return iconv($fromEncoding, $toEncoding.'//IGNORE', $s); } public static function mb_convert_variables($toEncoding, $fromEncoding, &$a = null, &$b = null, &$c = null, &$d = null, &$e = null, &$f = null) { $vars = array(&$a, &$b, &$c, &$d, &$e, &$f); $ok = true; array_walk_recursive($vars, function (&$v) use (&$ok, $toEncoding, $fromEncoding) { if (false === $v = Mbstring::mb_convert_encoding($v, $toEncoding, $fromEncoding)) { $ok = false; } }); return $ok ? $fromEncoding : false; } public static function mb_decode_mimeheader($s) { return iconv_mime_decode($s, 2, self::$internalEncoding); } public static function mb_encode_mimeheader($s, $charset = null, $transferEncoding = null, $linefeed = null, $indent = null) { trigger_error('mb_encode_mimeheader() is bugged. Please use iconv_mime_encode() instead', E_USER_WARNING); } public static function mb_convert_case($s, $mode, $encoding = null) { if ('' === $s .= '') { return ''; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding) { $encoding = null; } else { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } if (MB_CASE_TITLE == $mode) { $s = preg_replace_callback('/\b\p{Ll}/u', array(__CLASS__, 'title_case_upper'), $s); $s = preg_replace_callback('/\B[\p{Lu}\p{Lt}]+/u', array(__CLASS__, 'title_case_lower'), $s); } else { if (MB_CASE_UPPER == $mode) { static $upper = null; if (null === $upper) { $upper = self::getData('upperCase'); } $map = $upper; } else { if (self::MB_CASE_FOLD === $mode) { $s = str_replace(self::$caseFold[0], self::$caseFold[1], $s); } static $lower = null; if (null === $lower) { $lower = self::getData('lowerCase'); } $map = $lower; } static $ulenMask = array("\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4); $i = 0; $len = strlen($s); while ($i < $len) { $ulen = $s[$i] < "\x80" ? 1 : $ulenMask[$s[$i] & "\xF0"]; $uchr = substr($s, $i, $ulen); $i += $ulen; if (isset($map[$uchr])) { $uchr = $map[$uchr]; $nlen = strlen($uchr); if ($nlen == $ulen) { $nlen = $i; do { $s[--$nlen] = $uchr[--$ulen]; } while ($ulen); } else { $s = substr_replace($s, $uchr, $i - $ulen, $ulen); $len += $nlen - $ulen; $i += $nlen - $ulen; } } } } if (null === $encoding) { return $s; } return iconv('UTF-8', $encoding.'//IGNORE', $s); } public static function mb_internal_encoding($encoding = null) { if (null === $encoding) { return self::$internalEncoding; } $encoding = self::getEncoding($encoding); if ('UTF-8' === $encoding || false !== @iconv($encoding, $encoding, ' ')) { self::$internalEncoding = $encoding; return true; } return false; } public static function mb_language($lang = null) { if (null === $lang) { return self::$language; } switch ($lang = strtolower($lang)) { case 'uni': case 'neutral': self::$language = $lang; return true; } return false; } public static function mb_list_encodings() { return array('UTF-8'); } public static function mb_encoding_aliases($encoding) { switch (strtoupper($encoding)) { case 'UTF8': case 'UTF-8': return array('utf8'); } return false; } public static function mb_check_encoding($var = null, $encoding = null) { if (null === $encoding) { if (null === $var) { return false; } $encoding = self::$internalEncoding; } return self::mb_detect_encoding($var, array($encoding)) || false !== @iconv($encoding, $encoding, $var); } public static function mb_detect_encoding($str, $encodingList = null, $strict = false) { if (null === $encodingList) { $encodingList = self::$encodingList; } else { if (!is_array($encodingList)) { $encodingList = array_map('trim', explode(',', $encodingList)); } $encodingList = array_map('strtoupper', $encodingList); } foreach ($encodingList as $enc) { switch ($enc) { case 'ASCII': if (!preg_match('/[\x80-\xFF]/', $str)) { return $enc; } break; case 'UTF8': case 'UTF-8': if (preg_match('//u', $str)) { return 'UTF-8'; } break; default: if (0 === strncmp($enc, 'ISO-8859-', 9)) { return $enc; } } } return false; } public static function mb_detect_order($encodingList = null) { if (null === $encodingList) { return self::$encodingList; } if (!is_array($encodingList)) { $encodingList = array_map('trim', explode(',', $encodingList)); } $encodingList = array_map('strtoupper', $encodingList); foreach ($encodingList as $enc) { switch ($enc) { default: if (strncmp($enc, 'ISO-8859-', 9)) { return false; } case 'ASCII': case 'UTF8': case 'UTF-8': } } self::$encodingList = $encodingList; return true; } public static function mb_strlen($s, $encoding = null) { switch ($encoding = self::getEncoding($encoding)) { case 'ASCII': case 'CP850': return strlen($s); } return @iconv_strlen($s, $encoding); } public static function mb_strpos($haystack, $needle, $offset = 0, $encoding = null) { $encoding = self::getEncoding($encoding); if ('' === $needle .= '') { trigger_error(__METHOD__.': Empty delimiter', E_USER_WARNING); return false; } return iconv_strpos($haystack, $needle, $offset, $encoding); } public static function mb_strrpos($haystack, $needle, $offset = 0, $encoding = null) { $encoding = self::getEncoding($encoding); if ($offset != (int) $offset) { $offset = 0; } elseif ($offset = (int) $offset) { if ($offset < 0) { $haystack = self::mb_substr($haystack, 0, $offset, $encoding); $offset = 0; } else { $haystack = self::mb_substr($haystack, $offset, 2147483647, $encoding); } } $pos = iconv_strrpos($haystack, $needle, $encoding); return false !== $pos ? $offset + $pos : false; } public static function mb_strtolower($s, $encoding = null) { return self::mb_convert_case($s, MB_CASE_LOWER, $encoding); } public static function mb_strtoupper($s, $encoding = null) { return self::mb_convert_case($s, MB_CASE_UPPER, $encoding); } public static function mb_substitute_character($c = null) { if (0 === strcasecmp($c, 'none')) { return true; } return null !== $c ? false : 'none'; } public static function mb_substr($s, $start, $length = null, $encoding = null) { $encoding = self::getEncoding($encoding); if ($start < 0) { $start = iconv_strlen($s, $encoding) + $start; if ($start < 0) { $start = 0; } } if (null === $length) { $length = 2147483647; } elseif ($length < 0) { $length = iconv_strlen($s, $encoding) + $length - $start; if ($length < 0) { return ''; } } return iconv_substr($s, $start, $length, $encoding).''; } public static function mb_stripos($haystack, $needle, $offset = 0, $encoding = null) { $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); return self::mb_strpos($haystack, $needle, $offset, $encoding); } public static function mb_stristr($haystack, $needle, $part = false, $encoding = null) { $pos = self::mb_stripos($haystack, $needle, 0, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strrchr($haystack, $needle, $part = false, $encoding = null) { $encoding = self::getEncoding($encoding); $needle = self::mb_substr($needle, 0, 1, $encoding); $pos = iconv_strrpos($haystack, $needle, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strrichr($haystack, $needle, $part = false, $encoding = null) { $needle = self::mb_substr($needle, 0, 1, $encoding); $pos = self::mb_strripos($haystack, $needle, $encoding); return self::getSubpart($pos, $part, $haystack, $encoding); } public static function mb_strripos($haystack, $needle, $offset = 0, $encoding = null) { $haystack = self::mb_convert_case($haystack, self::MB_CASE_FOLD, $encoding); $needle = self::mb_convert_case($needle, self::MB_CASE_FOLD, $encoding); return self::mb_strrpos($haystack, $needle, $offset, $encoding); } public static function mb_strstr($haystack, $needle, $part = false, $encoding = null) { $pos = strpos($haystack, $needle); if (false === $pos) { return false; } if ($part) { return substr($haystack, 0, $pos); } return substr($haystack, $pos); } public static function mb_get_info($type = 'all') { $info = array( 'internal_encoding' => self::$internalEncoding, 'http_output' => 'pass', 'http_output_conv_mimetypes' => '^(text/|application/xhtml\+xml)', 'func_overload' => 0, 'func_overload_list' => 'no overload', 'mail_charset' => 'UTF-8', 'mail_header_encoding' => 'BASE64', 'mail_body_encoding' => 'BASE64', 'illegal_chars' => 0, 'encoding_translation' => 'Off', 'language' => self::$language, 'detect_order' => self::$encodingList, 'substitute_character' => 'none', 'strict_detection' => 'Off', ); if ('all' === $type) { return $info; } if (isset($info[$type])) { return $info[$type]; } return false; } public static function mb_http_input($type = '') { return false; } public static function mb_http_output($encoding = null) { return null !== $encoding ? 'pass' === $encoding : 'pass'; } public static function mb_strwidth($s, $encoding = null) { $encoding = self::getEncoding($encoding); if ('UTF-8' !== $encoding) { $s = iconv($encoding, 'UTF-8//IGNORE', $s); } $s = preg_replace('/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', '', $s, -1, $wide); return ($wide << 1) + iconv_strlen($s, 'UTF-8'); } public static function mb_substr_count($haystack, $needle, $encoding = null) { return substr_count($haystack, $needle); } public static function mb_output_handler($contents, $status) { return $contents; } public static function mb_chr($code, $encoding = null) { if (0x80 > $code %= 0x200000) { $s = chr($code); } elseif (0x800 > $code) { $s = chr(0xC0 | $code >> 6).chr(0x80 | $code & 0x3F); } elseif (0x10000 > $code) { $s = chr(0xE0 | $code >> 12).chr(0x80 | $code >> 6 & 0x3F).chr(0x80 | $code & 0x3F); } else { $s = chr(0xF0 | $code >> 18).chr(0x80 | $code >> 12 & 0x3F).chr(0x80 | $code >> 6 & 0x3F).chr(0x80 | $code & 0x3F); } if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { $s = mb_convert_encoding($s, $encoding, 'UTF-8'); } return $s; } public static function mb_ord($s, $encoding = null) { if ('UTF-8' !== $encoding = self::getEncoding($encoding)) { $s = mb_convert_encoding($s, 'UTF-8', $encoding); } $code = ($s = unpack('C*', substr($s, 0, 4))) ? $s[1] : 0; if (0xF0 <= $code) { return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; } if (0xE0 <= $code) { return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; } if (0xC0 <= $code) { return (($code - 0xC0) << 6) + $s[2] - 0x80; } return $code; } private static function getSubpart($pos, $part, $haystack, $encoding) { if (false === $pos) { return false; } if ($part) { return self::mb_substr($haystack, 0, $pos, $encoding); } return self::mb_substr($haystack, $pos, null, $encoding); } private static function html_encoding_callback($m) { $i = 1; $entities = ''; $m = unpack('C*', htmlentities($m[0], ENT_COMPAT, 'UTF-8')); while (isset($m[$i])) { if (0x80 > $m[$i]) { $entities .= chr($m[$i++]); continue; } if (0xF0 <= $m[$i]) { $c = (($m[$i++] - 0xF0) << 18) + (($m[$i++] - 0x80) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; } elseif (0xE0 <= $m[$i]) { $c = (($m[$i++] - 0xE0) << 12) + (($m[$i++] - 0x80) << 6) + $m[$i++] - 0x80; } else { $c = (($m[$i++] - 0xC0) << 6) + $m[$i++] - 0x80; } $entities .= '&#'.$c.';'; } return $entities; } private static function title_case_lower($s) { return self::mb_convert_case($s[0], MB_CASE_LOWER, 'UTF-8'); } private static function title_case_upper($s) { return self::mb_convert_case($s[0], MB_CASE_UPPER, 'UTF-8'); } private static function getData($file) { if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { return require $file; } return false; } private static function getEncoding($encoding) { if (null === $encoding) { return self::$internalEncoding; } $encoding = strtoupper($encoding); if ('8BIT' === $encoding || 'BINARY' === $encoding) { return 'CP850'; } if ('UTF8' === $encoding) { return 'UTF-8'; } return $encoding; } } 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n', 'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z', 'À' => 'à', 'Á' => 'á', 'Â' => 'â', 'Ã' => 'ã', 'Ä' => 'ä', 'Å' => 'å', 'Æ' => 'æ', 'Ç' => 'ç', 'È' => 'è', 'É' => 'é', 'Ê' => 'ê', 'Ë' => 'ë', 'Ì' => 'ì', 'Í' => 'í', 'Î' => 'î', 'Ï' => 'ï', 'Ð' => 'ð', 'Ñ' => 'ñ', 'Ò' => 'ò', 'Ó' => 'ó', 'Ô' => 'ô', 'Õ' => 'õ', 'Ö' => 'ö', 'Ø' => 'ø', 'Ù' => 'ù', 'Ú' => 'ú', 'Û' => 'û', 'Ü' => 'ü', 'Ý' => 'ý', 'Þ' => 'þ', 'Ā' => 'ā', 'Ă' => 'ă', 'Ą' => 'ą', 'Ć' => 'ć', 'Ĉ' => 'ĉ', 'Ċ' => 'ċ', 'Č' => 'č', 'Ď' => 'ď', 'Đ' => 'đ', 'Ē' => 'ē', 'Ĕ' => 'ĕ', 'Ė' => 'ė', 'Ę' => 'ę', 'Ě' => 'ě', 'Ĝ' => 'ĝ', 'Ğ' => 'ğ', 'Ġ' => 'ġ', 'Ģ' => 'ģ', 'Ĥ' => 'ĥ', 'Ħ' => 'ħ', 'Ĩ' => 'ĩ', 'Ī' => 'ī', 'Ĭ' => 'ĭ', 'Į' => 'į', 'İ' => 'i', 'IJ' => 'ij', 'Ĵ' => 'ĵ', 'Ķ' => 'ķ', 'Ĺ' => 'ĺ', 'Ļ' => 'ļ', 'Ľ' => 'ľ', 'Ŀ' => 'ŀ', 'Ł' => 'ł', 'Ń' => 'ń', 'Ņ' => 'ņ', 'Ň' => 'ň', 'Ŋ' => 'ŋ', 'Ō' => 'ō', 'Ŏ' => 'ŏ', 'Ő' => 'ő', 'Œ' => 'œ', 'Ŕ' => 'ŕ', 'Ŗ' => 'ŗ', 'Ř' => 'ř', 'Ś' => 'ś', 'Ŝ' => 'ŝ', 'Ş' => 'ş', 'Š' => 'š', 'Ţ' => 'ţ', 'Ť' => 'ť', 'Ŧ' => 'ŧ', 'Ũ' => 'ũ', 'Ū' => 'ū', 'Ŭ' => 'ŭ', 'Ů' => 'ů', 'Ű' => 'ű', 'Ų' => 'ų', 'Ŵ' => 'ŵ', 'Ŷ' => 'ŷ', 'Ÿ' => 'ÿ', 'Ź' => 'ź', 'Ż' => 'ż', 'Ž' => 'ž', 'Ɓ' => 'ɓ', 'Ƃ' => 'ƃ', 'Ƅ' => 'ƅ', 'Ɔ' => 'ɔ', 'Ƈ' => 'ƈ', 'Ɖ' => 'ɖ', 'Ɗ' => 'ɗ', 'Ƌ' => 'ƌ', 'Ǝ' => 'ǝ', 'Ə' => 'ə', 'Ɛ' => 'ɛ', 'Ƒ' => 'ƒ', 'Ɠ' => 'ɠ', 'Ɣ' => 'ɣ', 'Ɩ' => 'ɩ', 'Ɨ' => 'ɨ', 'Ƙ' => 'ƙ', 'Ɯ' => 'ɯ', 'Ɲ' => 'ɲ', 'Ɵ' => 'ɵ', 'Ơ' => 'ơ', 'Ƣ' => 'ƣ', 'Ƥ' => 'ƥ', 'Ʀ' => 'ʀ', 'Ƨ' => 'ƨ', 'Ʃ' => 'ʃ', 'Ƭ' => 'ƭ', 'Ʈ' => 'ʈ', 'Ư' => 'ư', 'Ʊ' => 'ʊ', 'Ʋ' => 'ʋ', 'Ƴ' => 'ƴ', 'Ƶ' => 'ƶ', 'Ʒ' => 'ʒ', 'Ƹ' => 'ƹ', 'Ƽ' => 'ƽ', 'DŽ' => 'dž', 'Dž' => 'dž', 'LJ' => 'lj', 'Lj' => 'lj', 'NJ' => 'nj', 'Nj' => 'nj', 'Ǎ' => 'ǎ', 'Ǐ' => 'ǐ', 'Ǒ' => 'ǒ', 'Ǔ' => 'ǔ', 'Ǖ' => 'ǖ', 'Ǘ' => 'ǘ', 'Ǚ' => 'ǚ', 'Ǜ' => 'ǜ', 'Ǟ' => 'ǟ', 'Ǡ' => 'ǡ', 'Ǣ' => 'ǣ', 'Ǥ' => 'ǥ', 'Ǧ' => 'ǧ', 'Ǩ' => 'ǩ', 'Ǫ' => 'ǫ', 'Ǭ' => 'ǭ', 'Ǯ' => 'ǯ', 'DZ' => 'dz', 'Dz' => 'dz', 'Ǵ' => 'ǵ', 'Ƕ' => 'ƕ', 'Ƿ' => 'ƿ', 'Ǹ' => 'ǹ', 'Ǻ' => 'ǻ', 'Ǽ' => 'ǽ', 'Ǿ' => 'ǿ', 'Ȁ' => 'ȁ', 'Ȃ' => 'ȃ', 'Ȅ' => 'ȅ', 'Ȇ' => 'ȇ', 'Ȉ' => 'ȉ', 'Ȋ' => 'ȋ', 'Ȍ' => 'ȍ', 'Ȏ' => 'ȏ', 'Ȑ' => 'ȑ', 'Ȓ' => 'ȓ', 'Ȕ' => 'ȕ', 'Ȗ' => 'ȗ', 'Ș' => 'ș', 'Ț' => 'ț', 'Ȝ' => 'ȝ', 'Ȟ' => 'ȟ', 'Ƞ' => 'ƞ', 'Ȣ' => 'ȣ', 'Ȥ' => 'ȥ', 'Ȧ' => 'ȧ', 'Ȩ' => 'ȩ', 'Ȫ' => 'ȫ', 'Ȭ' => 'ȭ', 'Ȯ' => 'ȯ', 'Ȱ' => 'ȱ', 'Ȳ' => 'ȳ', 'Ⱥ' => 'ⱥ', 'Ȼ' => 'ȼ', 'Ƚ' => 'ƚ', 'Ⱦ' => 'ⱦ', 'Ɂ' => 'ɂ', 'Ƀ' => 'ƀ', 'Ʉ' => 'ʉ', 'Ʌ' => 'ʌ', 'Ɇ' => 'ɇ', 'Ɉ' => 'ɉ', 'Ɋ' => 'ɋ', 'Ɍ' => 'ɍ', 'Ɏ' => 'ɏ', 'Ͱ' => 'ͱ', 'Ͳ' => 'ͳ', 'Ͷ' => 'ͷ', 'Ϳ' => 'ϳ', 'Ά' => 'ά', 'Έ' => 'έ', 'Ή' => 'ή', 'Ί' => 'ί', 'Ό' => 'ό', 'Ύ' => 'ύ', 'Ώ' => 'ώ', 'Α' => 'α', 'Β' => 'β', 'Γ' => 'γ', 'Δ' => 'δ', 'Ε' => 'ε', 'Ζ' => 'ζ', 'Η' => 'η', 'Θ' => 'θ', 'Ι' => 'ι', 'Κ' => 'κ', 'Λ' => 'λ', 'Μ' => 'μ', 'Ν' => 'ν', 'Ξ' => 'ξ', 'Ο' => 'ο', 'Π' => 'π', 'Ρ' => 'ρ', 'Σ' => 'σ', 'Τ' => 'τ', 'Υ' => 'υ', 'Φ' => 'φ', 'Χ' => 'χ', 'Ψ' => 'ψ', 'Ω' => 'ω', 'Ϊ' => 'ϊ', 'Ϋ' => 'ϋ', 'Ϗ' => 'ϗ', 'Ϙ' => 'ϙ', 'Ϛ' => 'ϛ', 'Ϝ' => 'ϝ', 'Ϟ' => 'ϟ', 'Ϡ' => 'ϡ', 'Ϣ' => 'ϣ', 'Ϥ' => 'ϥ', 'Ϧ' => 'ϧ', 'Ϩ' => 'ϩ', 'Ϫ' => 'ϫ', 'Ϭ' => 'ϭ', 'Ϯ' => 'ϯ', 'ϴ' => 'θ', 'Ϸ' => 'ϸ', 'Ϲ' => 'ϲ', 'Ϻ' => 'ϻ', 'Ͻ' => 'ͻ', 'Ͼ' => 'ͼ', 'Ͽ' => 'ͽ', 'Ѐ' => 'ѐ', 'Ё' => 'ё', 'Ђ' => 'ђ', 'Ѓ' => 'ѓ', 'Є' => 'є', 'Ѕ' => 'ѕ', 'І' => 'і', 'Ї' => 'ї', 'Ј' => 'ј', 'Љ' => 'љ', 'Њ' => 'њ', 'Ћ' => 'ћ', 'Ќ' => 'ќ', 'Ѝ' => 'ѝ', 'Ў' => 'ў', 'Џ' => 'џ', 'А' => 'а', 'Б' => 'б', 'В' => 'в', 'Г' => 'г', 'Д' => 'д', 'Е' => 'е', 'Ж' => 'ж', 'З' => 'з', 'И' => 'и', 'Й' => 'й', 'К' => 'к', 'Л' => 'л', 'М' => 'м', 'Н' => 'н', 'О' => 'о', 'П' => 'п', 'Р' => 'р', 'С' => 'с', 'Т' => 'т', 'У' => 'у', 'Ф' => 'ф', 'Х' => 'х', 'Ц' => 'ц', 'Ч' => 'ч', 'Ш' => 'ш', 'Щ' => 'щ', 'Ъ' => 'ъ', 'Ы' => 'ы', 'Ь' => 'ь', 'Э' => 'э', 'Ю' => 'ю', 'Я' => 'я', 'Ѡ' => 'ѡ', 'Ѣ' => 'ѣ', 'Ѥ' => 'ѥ', 'Ѧ' => 'ѧ', 'Ѩ' => 'ѩ', 'Ѫ' => 'ѫ', 'Ѭ' => 'ѭ', 'Ѯ' => 'ѯ', 'Ѱ' => 'ѱ', 'Ѳ' => 'ѳ', 'Ѵ' => 'ѵ', 'Ѷ' => 'ѷ', 'Ѹ' => 'ѹ', 'Ѻ' => 'ѻ', 'Ѽ' => 'ѽ', 'Ѿ' => 'ѿ', 'Ҁ' => 'ҁ', 'Ҋ' => 'ҋ', 'Ҍ' => 'ҍ', 'Ҏ' => 'ҏ', 'Ґ' => 'ґ', 'Ғ' => 'ғ', 'Ҕ' => 'ҕ', 'Җ' => 'җ', 'Ҙ' => 'ҙ', 'Қ' => 'қ', 'Ҝ' => 'ҝ', 'Ҟ' => 'ҟ', 'Ҡ' => 'ҡ', 'Ң' => 'ң', 'Ҥ' => 'ҥ', 'Ҧ' => 'ҧ', 'Ҩ' => 'ҩ', 'Ҫ' => 'ҫ', 'Ҭ' => 'ҭ', 'Ү' => 'ү', 'Ұ' => 'ұ', 'Ҳ' => 'ҳ', 'Ҵ' => 'ҵ', 'Ҷ' => 'ҷ', 'Ҹ' => 'ҹ', 'Һ' => 'һ', 'Ҽ' => 'ҽ', 'Ҿ' => 'ҿ', 'Ӏ' => 'ӏ', 'Ӂ' => 'ӂ', 'Ӄ' => 'ӄ', 'Ӆ' => 'ӆ', 'Ӈ' => 'ӈ', 'Ӊ' => 'ӊ', 'Ӌ' => 'ӌ', 'Ӎ' => 'ӎ', 'Ӑ' => 'ӑ', 'Ӓ' => 'ӓ', 'Ӕ' => 'ӕ', 'Ӗ' => 'ӗ', 'Ә' => 'ә', 'Ӛ' => 'ӛ', 'Ӝ' => 'ӝ', 'Ӟ' => 'ӟ', 'Ӡ' => 'ӡ', 'Ӣ' => 'ӣ', 'Ӥ' => 'ӥ', 'Ӧ' => 'ӧ', 'Ө' => 'ө', 'Ӫ' => 'ӫ', 'Ӭ' => 'ӭ', 'Ӯ' => 'ӯ', 'Ӱ' => 'ӱ', 'Ӳ' => 'ӳ', 'Ӵ' => 'ӵ', 'Ӷ' => 'ӷ', 'Ӹ' => 'ӹ', 'Ӻ' => 'ӻ', 'Ӽ' => 'ӽ', 'Ӿ' => 'ӿ', 'Ԁ' => 'ԁ', 'Ԃ' => 'ԃ', 'Ԅ' => 'ԅ', 'Ԇ' => 'ԇ', 'Ԉ' => 'ԉ', 'Ԋ' => 'ԋ', 'Ԍ' => 'ԍ', 'Ԏ' => 'ԏ', 'Ԑ' => 'ԑ', 'Ԓ' => 'ԓ', 'Ԕ' => 'ԕ', 'Ԗ' => 'ԗ', 'Ԙ' => 'ԙ', 'Ԛ' => 'ԛ', 'Ԝ' => 'ԝ', 'Ԟ' => 'ԟ', 'Ԡ' => 'ԡ', 'Ԣ' => 'ԣ', 'Ԥ' => 'ԥ', 'Ԧ' => 'ԧ', 'Ԩ' => 'ԩ', 'Ԫ' => 'ԫ', 'Ԭ' => 'ԭ', 'Ԯ' => 'ԯ', 'Ա' => 'ա', 'Բ' => 'բ', 'Գ' => 'գ', 'Դ' => 'դ', 'Ե' => 'ե', 'Զ' => 'զ', 'Է' => 'է', 'Ը' => 'ը', 'Թ' => 'թ', 'Ժ' => 'ժ', 'Ի' => 'ի', 'Լ' => 'լ', 'Խ' => 'խ', 'Ծ' => 'ծ', 'Կ' => 'կ', 'Հ' => 'հ', 'Ձ' => 'ձ', 'Ղ' => 'ղ', 'Ճ' => 'ճ', 'Մ' => 'մ', 'Յ' => 'յ', 'Ն' => 'ն', 'Շ' => 'շ', 'Ո' => 'ո', 'Չ' => 'չ', 'Պ' => 'պ', 'Ջ' => 'ջ', 'Ռ' => 'ռ', 'Ս' => 'ս', 'Վ' => 'վ', 'Տ' => 'տ', 'Ր' => 'ր', 'Ց' => 'ց', 'Ւ' => 'ւ', 'Փ' => 'փ', 'Ք' => 'ք', 'Օ' => 'օ', 'Ֆ' => 'ֆ', 'Ⴀ' => 'ⴀ', 'Ⴁ' => 'ⴁ', 'Ⴂ' => 'ⴂ', 'Ⴃ' => 'ⴃ', 'Ⴄ' => 'ⴄ', 'Ⴅ' => 'ⴅ', 'Ⴆ' => 'ⴆ', 'Ⴇ' => 'ⴇ', 'Ⴈ' => 'ⴈ', 'Ⴉ' => 'ⴉ', 'Ⴊ' => 'ⴊ', 'Ⴋ' => 'ⴋ', 'Ⴌ' => 'ⴌ', 'Ⴍ' => 'ⴍ', 'Ⴎ' => 'ⴎ', 'Ⴏ' => 'ⴏ', 'Ⴐ' => 'ⴐ', 'Ⴑ' => 'ⴑ', 'Ⴒ' => 'ⴒ', 'Ⴓ' => 'ⴓ', 'Ⴔ' => 'ⴔ', 'Ⴕ' => 'ⴕ', 'Ⴖ' => 'ⴖ', 'Ⴗ' => 'ⴗ', 'Ⴘ' => 'ⴘ', 'Ⴙ' => 'ⴙ', 'Ⴚ' => 'ⴚ', 'Ⴛ' => 'ⴛ', 'Ⴜ' => 'ⴜ', 'Ⴝ' => 'ⴝ', 'Ⴞ' => 'ⴞ', 'Ⴟ' => 'ⴟ', 'Ⴠ' => 'ⴠ', 'Ⴡ' => 'ⴡ', 'Ⴢ' => 'ⴢ', 'Ⴣ' => 'ⴣ', 'Ⴤ' => 'ⴤ', 'Ⴥ' => 'ⴥ', 'Ⴧ' => 'ⴧ', 'Ⴭ' => 'ⴭ', 'Ḁ' => 'ḁ', 'Ḃ' => 'ḃ', 'Ḅ' => 'ḅ', 'Ḇ' => 'ḇ', 'Ḉ' => 'ḉ', 'Ḋ' => 'ḋ', 'Ḍ' => 'ḍ', 'Ḏ' => 'ḏ', 'Ḑ' => 'ḑ', 'Ḓ' => 'ḓ', 'Ḕ' => 'ḕ', 'Ḗ' => 'ḗ', 'Ḙ' => 'ḙ', 'Ḛ' => 'ḛ', 'Ḝ' => 'ḝ', 'Ḟ' => 'ḟ', 'Ḡ' => 'ḡ', 'Ḣ' => 'ḣ', 'Ḥ' => 'ḥ', 'Ḧ' => 'ḧ', 'Ḩ' => 'ḩ', 'Ḫ' => 'ḫ', 'Ḭ' => 'ḭ', 'Ḯ' => 'ḯ', 'Ḱ' => 'ḱ', 'Ḳ' => 'ḳ', 'Ḵ' => 'ḵ', 'Ḷ' => 'ḷ', 'Ḹ' => 'ḹ', 'Ḻ' => 'ḻ', 'Ḽ' => 'ḽ', 'Ḿ' => 'ḿ', 'Ṁ' => 'ṁ', 'Ṃ' => 'ṃ', 'Ṅ' => 'ṅ', 'Ṇ' => 'ṇ', 'Ṉ' => 'ṉ', 'Ṋ' => 'ṋ', 'Ṍ' => 'ṍ', 'Ṏ' => 'ṏ', 'Ṑ' => 'ṑ', 'Ṓ' => 'ṓ', 'Ṕ' => 'ṕ', 'Ṗ' => 'ṗ', 'Ṙ' => 'ṙ', 'Ṛ' => 'ṛ', 'Ṝ' => 'ṝ', 'Ṟ' => 'ṟ', 'Ṡ' => 'ṡ', 'Ṣ' => 'ṣ', 'Ṥ' => 'ṥ', 'Ṧ' => 'ṧ', 'Ṩ' => 'ṩ', 'Ṫ' => 'ṫ', 'Ṭ' => 'ṭ', 'Ṯ' => 'ṯ', 'Ṱ' => 'ṱ', 'Ṳ' => 'ṳ', 'Ṵ' => 'ṵ', 'Ṷ' => 'ṷ', 'Ṹ' => 'ṹ', 'Ṻ' => 'ṻ', 'Ṽ' => 'ṽ', 'Ṿ' => 'ṿ', 'Ẁ' => 'ẁ', 'Ẃ' => 'ẃ', 'Ẅ' => 'ẅ', 'Ẇ' => 'ẇ', 'Ẉ' => 'ẉ', 'Ẋ' => 'ẋ', 'Ẍ' => 'ẍ', 'Ẏ' => 'ẏ', 'Ẑ' => 'ẑ', 'Ẓ' => 'ẓ', 'Ẕ' => 'ẕ', 'ẞ' => 'ß', 'Ạ' => 'ạ', 'Ả' => 'ả', 'Ấ' => 'ấ', 'Ầ' => 'ầ', 'Ẩ' => 'ẩ', 'Ẫ' => 'ẫ', 'Ậ' => 'ậ', 'Ắ' => 'ắ', 'Ằ' => 'ằ', 'Ẳ' => 'ẳ', 'Ẵ' => 'ẵ', 'Ặ' => 'ặ', 'Ẹ' => 'ẹ', 'Ẻ' => 'ẻ', 'Ẽ' => 'ẽ', 'Ế' => 'ế', 'Ề' => 'ề', 'Ể' => 'ể', 'Ễ' => 'ễ', 'Ệ' => 'ệ', 'Ỉ' => 'ỉ', 'Ị' => 'ị', 'Ọ' => 'ọ', 'Ỏ' => 'ỏ', 'Ố' => 'ố', 'Ồ' => 'ồ', 'Ổ' => 'ổ', 'Ỗ' => 'ỗ', 'Ộ' => 'ộ', 'Ớ' => 'ớ', 'Ờ' => 'ờ', 'Ở' => 'ở', 'Ỡ' => 'ỡ', 'Ợ' => 'ợ', 'Ụ' => 'ụ', 'Ủ' => 'ủ', 'Ứ' => 'ứ', 'Ừ' => 'ừ', 'Ử' => 'ử', 'Ữ' => 'ữ', 'Ự' => 'ự', 'Ỳ' => 'ỳ', 'Ỵ' => 'ỵ', 'Ỷ' => 'ỷ', 'Ỹ' => 'ỹ', 'Ỻ' => 'ỻ', 'Ỽ' => 'ỽ', 'Ỿ' => 'ỿ', 'Ἀ' => 'ἀ', 'Ἁ' => 'ἁ', 'Ἂ' => 'ἂ', 'Ἃ' => 'ἃ', 'Ἄ' => 'ἄ', 'Ἅ' => 'ἅ', 'Ἆ' => 'ἆ', 'Ἇ' => 'ἇ', 'Ἐ' => 'ἐ', 'Ἑ' => 'ἑ', 'Ἒ' => 'ἒ', 'Ἓ' => 'ἓ', 'Ἔ' => 'ἔ', 'Ἕ' => 'ἕ', 'Ἠ' => 'ἠ', 'Ἡ' => 'ἡ', 'Ἢ' => 'ἢ', 'Ἣ' => 'ἣ', 'Ἤ' => 'ἤ', 'Ἥ' => 'ἥ', 'Ἦ' => 'ἦ', 'Ἧ' => 'ἧ', 'Ἰ' => 'ἰ', 'Ἱ' => 'ἱ', 'Ἲ' => 'ἲ', 'Ἳ' => 'ἳ', 'Ἴ' => 'ἴ', 'Ἵ' => 'ἵ', 'Ἶ' => 'ἶ', 'Ἷ' => 'ἷ', 'Ὀ' => 'ὀ', 'Ὁ' => 'ὁ', 'Ὂ' => 'ὂ', 'Ὃ' => 'ὃ', 'Ὄ' => 'ὄ', 'Ὅ' => 'ὅ', 'Ὑ' => 'ὑ', 'Ὓ' => 'ὓ', 'Ὕ' => 'ὕ', 'Ὗ' => 'ὗ', 'Ὠ' => 'ὠ', 'Ὡ' => 'ὡ', 'Ὢ' => 'ὢ', 'Ὣ' => 'ὣ', 'Ὤ' => 'ὤ', 'Ὥ' => 'ὥ', 'Ὦ' => 'ὦ', 'Ὧ' => 'ὧ', 'ᾈ' => 'ᾀ', 'ᾉ' => 'ᾁ', 'ᾊ' => 'ᾂ', 'ᾋ' => 'ᾃ', 'ᾌ' => 'ᾄ', 'ᾍ' => 'ᾅ', 'ᾎ' => 'ᾆ', 'ᾏ' => 'ᾇ', 'ᾘ' => 'ᾐ', 'ᾙ' => 'ᾑ', 'ᾚ' => 'ᾒ', 'ᾛ' => 'ᾓ', 'ᾜ' => 'ᾔ', 'ᾝ' => 'ᾕ', 'ᾞ' => 'ᾖ', 'ᾟ' => 'ᾗ', 'ᾨ' => 'ᾠ', 'ᾩ' => 'ᾡ', 'ᾪ' => 'ᾢ', 'ᾫ' => 'ᾣ', 'ᾬ' => 'ᾤ', 'ᾭ' => 'ᾥ', 'ᾮ' => 'ᾦ', 'ᾯ' => 'ᾧ', 'Ᾰ' => 'ᾰ', 'Ᾱ' => 'ᾱ', 'Ὰ' => 'ὰ', 'Ά' => 'ά', 'ᾼ' => 'ᾳ', 'Ὲ' => 'ὲ', 'Έ' => 'έ', 'Ὴ' => 'ὴ', 'Ή' => 'ή', 'ῌ' => 'ῃ', 'Ῐ' => 'ῐ', 'Ῑ' => 'ῑ', 'Ὶ' => 'ὶ', 'Ί' => 'ί', 'Ῠ' => 'ῠ', 'Ῡ' => 'ῡ', 'Ὺ' => 'ὺ', 'Ύ' => 'ύ', 'Ῥ' => 'ῥ', 'Ὸ' => 'ὸ', 'Ό' => 'ό', 'Ὼ' => 'ὼ', 'Ώ' => 'ώ', 'ῼ' => 'ῳ', 'Ω' => 'ω', 'K' => 'k', 'Å' => 'å', 'Ⅎ' => 'ⅎ', 'Ⅰ' => 'ⅰ', 'Ⅱ' => 'ⅱ', 'Ⅲ' => 'ⅲ', 'Ⅳ' => 'ⅳ', 'Ⅴ' => 'ⅴ', 'Ⅵ' => 'ⅵ', 'Ⅶ' => 'ⅶ', 'Ⅷ' => 'ⅷ', 'Ⅸ' => 'ⅸ', 'Ⅹ' => 'ⅹ', 'Ⅺ' => 'ⅺ', 'Ⅻ' => 'ⅻ', 'Ⅼ' => 'ⅼ', 'Ⅽ' => 'ⅽ', 'Ⅾ' => 'ⅾ', 'Ⅿ' => 'ⅿ', 'Ↄ' => 'ↄ', 'Ⓐ' => 'ⓐ', 'Ⓑ' => 'ⓑ', 'Ⓒ' => 'ⓒ', 'Ⓓ' => 'ⓓ', 'Ⓔ' => 'ⓔ', 'Ⓕ' => 'ⓕ', 'Ⓖ' => 'ⓖ', 'Ⓗ' => 'ⓗ', 'Ⓘ' => 'ⓘ', 'Ⓙ' => 'ⓙ', 'Ⓚ' => 'ⓚ', 'Ⓛ' => 'ⓛ', 'Ⓜ' => 'ⓜ', 'Ⓝ' => 'ⓝ', 'Ⓞ' => 'ⓞ', 'Ⓟ' => 'ⓟ', 'Ⓠ' => 'ⓠ', 'Ⓡ' => 'ⓡ', 'Ⓢ' => 'ⓢ', 'Ⓣ' => 'ⓣ', 'Ⓤ' => 'ⓤ', 'Ⓥ' => 'ⓥ', 'Ⓦ' => 'ⓦ', 'Ⓧ' => 'ⓧ', 'Ⓨ' => 'ⓨ', 'Ⓩ' => 'ⓩ', 'Ⰰ' => 'ⰰ', 'Ⰱ' => 'ⰱ', 'Ⰲ' => 'ⰲ', 'Ⰳ' => 'ⰳ', 'Ⰴ' => 'ⰴ', 'Ⰵ' => 'ⰵ', 'Ⰶ' => 'ⰶ', 'Ⰷ' => 'ⰷ', 'Ⰸ' => 'ⰸ', 'Ⰹ' => 'ⰹ', 'Ⰺ' => 'ⰺ', 'Ⰻ' => 'ⰻ', 'Ⰼ' => 'ⰼ', 'Ⰽ' => 'ⰽ', 'Ⰾ' => 'ⰾ', 'Ⰿ' => 'ⰿ', 'Ⱀ' => 'ⱀ', 'Ⱁ' => 'ⱁ', 'Ⱂ' => 'ⱂ', 'Ⱃ' => 'ⱃ', 'Ⱄ' => 'ⱄ', 'Ⱅ' => 'ⱅ', 'Ⱆ' => 'ⱆ', 'Ⱇ' => 'ⱇ', 'Ⱈ' => 'ⱈ', 'Ⱉ' => 'ⱉ', 'Ⱊ' => 'ⱊ', 'Ⱋ' => 'ⱋ', 'Ⱌ' => 'ⱌ', 'Ⱍ' => 'ⱍ', 'Ⱎ' => 'ⱎ', 'Ⱏ' => 'ⱏ', 'Ⱐ' => 'ⱐ', 'Ⱑ' => 'ⱑ', 'Ⱒ' => 'ⱒ', 'Ⱓ' => 'ⱓ', 'Ⱔ' => 'ⱔ', 'Ⱕ' => 'ⱕ', 'Ⱖ' => 'ⱖ', 'Ⱗ' => 'ⱗ', 'Ⱘ' => 'ⱘ', 'Ⱙ' => 'ⱙ', 'Ⱚ' => 'ⱚ', 'Ⱛ' => 'ⱛ', 'Ⱜ' => 'ⱜ', 'Ⱝ' => 'ⱝ', 'Ⱞ' => 'ⱞ', 'Ⱡ' => 'ⱡ', 'Ɫ' => 'ɫ', 'Ᵽ' => 'ᵽ', 'Ɽ' => 'ɽ', 'Ⱨ' => 'ⱨ', 'Ⱪ' => 'ⱪ', 'Ⱬ' => 'ⱬ', 'Ɑ' => 'ɑ', 'Ɱ' => 'ɱ', 'Ɐ' => 'ɐ', 'Ɒ' => 'ɒ', 'Ⱳ' => 'ⱳ', 'Ⱶ' => 'ⱶ', 'Ȿ' => 'ȿ', 'Ɀ' => 'ɀ', 'Ⲁ' => 'ⲁ', 'Ⲃ' => 'ⲃ', 'Ⲅ' => 'ⲅ', 'Ⲇ' => 'ⲇ', 'Ⲉ' => 'ⲉ', 'Ⲋ' => 'ⲋ', 'Ⲍ' => 'ⲍ', 'Ⲏ' => 'ⲏ', 'Ⲑ' => 'ⲑ', 'Ⲓ' => 'ⲓ', 'Ⲕ' => 'ⲕ', 'Ⲗ' => 'ⲗ', 'Ⲙ' => 'ⲙ', 'Ⲛ' => 'ⲛ', 'Ⲝ' => 'ⲝ', 'Ⲟ' => 'ⲟ', 'Ⲡ' => 'ⲡ', 'Ⲣ' => 'ⲣ', 'Ⲥ' => 'ⲥ', 'Ⲧ' => 'ⲧ', 'Ⲩ' => 'ⲩ', 'Ⲫ' => 'ⲫ', 'Ⲭ' => 'ⲭ', 'Ⲯ' => 'ⲯ', 'Ⲱ' => 'ⲱ', 'Ⲳ' => 'ⲳ', 'Ⲵ' => 'ⲵ', 'Ⲷ' => 'ⲷ', 'Ⲹ' => 'ⲹ', 'Ⲻ' => 'ⲻ', 'Ⲽ' => 'ⲽ', 'Ⲿ' => 'ⲿ', 'Ⳁ' => 'ⳁ', 'Ⳃ' => 'ⳃ', 'Ⳅ' => 'ⳅ', 'Ⳇ' => 'ⳇ', 'Ⳉ' => 'ⳉ', 'Ⳋ' => 'ⳋ', 'Ⳍ' => 'ⳍ', 'Ⳏ' => 'ⳏ', 'Ⳑ' => 'ⳑ', 'Ⳓ' => 'ⳓ', 'Ⳕ' => 'ⳕ', 'Ⳗ' => 'ⳗ', 'Ⳙ' => 'ⳙ', 'Ⳛ' => 'ⳛ', 'Ⳝ' => 'ⳝ', 'Ⳟ' => 'ⳟ', 'Ⳡ' => 'ⳡ', 'Ⳣ' => 'ⳣ', 'Ⳬ' => 'ⳬ', 'Ⳮ' => 'ⳮ', 'Ⳳ' => 'ⳳ', 'Ꙁ' => 'ꙁ', 'Ꙃ' => 'ꙃ', 'Ꙅ' => 'ꙅ', 'Ꙇ' => 'ꙇ', 'Ꙉ' => 'ꙉ', 'Ꙋ' => 'ꙋ', 'Ꙍ' => 'ꙍ', 'Ꙏ' => 'ꙏ', 'Ꙑ' => 'ꙑ', 'Ꙓ' => 'ꙓ', 'Ꙕ' => 'ꙕ', 'Ꙗ' => 'ꙗ', 'Ꙙ' => 'ꙙ', 'Ꙛ' => 'ꙛ', 'Ꙝ' => 'ꙝ', 'Ꙟ' => 'ꙟ', 'Ꙡ' => 'ꙡ', 'Ꙣ' => 'ꙣ', 'Ꙥ' => 'ꙥ', 'Ꙧ' => 'ꙧ', 'Ꙩ' => 'ꙩ', 'Ꙫ' => 'ꙫ', 'Ꙭ' => 'ꙭ', 'Ꚁ' => 'ꚁ', 'Ꚃ' => 'ꚃ', 'Ꚅ' => 'ꚅ', 'Ꚇ' => 'ꚇ', 'Ꚉ' => 'ꚉ', 'Ꚋ' => 'ꚋ', 'Ꚍ' => 'ꚍ', 'Ꚏ' => 'ꚏ', 'Ꚑ' => 'ꚑ', 'Ꚓ' => 'ꚓ', 'Ꚕ' => 'ꚕ', 'Ꚗ' => 'ꚗ', 'Ꚙ' => 'ꚙ', 'Ꚛ' => 'ꚛ', 'Ꜣ' => 'ꜣ', 'Ꜥ' => 'ꜥ', 'Ꜧ' => 'ꜧ', 'Ꜩ' => 'ꜩ', 'Ꜫ' => 'ꜫ', 'Ꜭ' => 'ꜭ', 'Ꜯ' => 'ꜯ', 'Ꜳ' => 'ꜳ', 'Ꜵ' => 'ꜵ', 'Ꜷ' => 'ꜷ', 'Ꜹ' => 'ꜹ', 'Ꜻ' => 'ꜻ', 'Ꜽ' => 'ꜽ', 'Ꜿ' => 'ꜿ', 'Ꝁ' => 'ꝁ', 'Ꝃ' => 'ꝃ', 'Ꝅ' => 'ꝅ', 'Ꝇ' => 'ꝇ', 'Ꝉ' => 'ꝉ', 'Ꝋ' => 'ꝋ', 'Ꝍ' => 'ꝍ', 'Ꝏ' => 'ꝏ', 'Ꝑ' => 'ꝑ', 'Ꝓ' => 'ꝓ', 'Ꝕ' => 'ꝕ', 'Ꝗ' => 'ꝗ', 'Ꝙ' => 'ꝙ', 'Ꝛ' => 'ꝛ', 'Ꝝ' => 'ꝝ', 'Ꝟ' => 'ꝟ', 'Ꝡ' => 'ꝡ', 'Ꝣ' => 'ꝣ', 'Ꝥ' => 'ꝥ', 'Ꝧ' => 'ꝧ', 'Ꝩ' => 'ꝩ', 'Ꝫ' => 'ꝫ', 'Ꝭ' => 'ꝭ', 'Ꝯ' => 'ꝯ', 'Ꝺ' => 'ꝺ', 'Ꝼ' => 'ꝼ', 'Ᵹ' => 'ᵹ', 'Ꝿ' => 'ꝿ', 'Ꞁ' => 'ꞁ', 'Ꞃ' => 'ꞃ', 'Ꞅ' => 'ꞅ', 'Ꞇ' => 'ꞇ', 'Ꞌ' => 'ꞌ', 'Ɥ' => 'ɥ', 'Ꞑ' => 'ꞑ', 'Ꞓ' => 'ꞓ', 'Ꞗ' => 'ꞗ', 'Ꞙ' => 'ꞙ', 'Ꞛ' => 'ꞛ', 'Ꞝ' => 'ꞝ', 'Ꞟ' => 'ꞟ', 'Ꞡ' => 'ꞡ', 'Ꞣ' => 'ꞣ', 'Ꞥ' => 'ꞥ', 'Ꞧ' => 'ꞧ', 'Ꞩ' => 'ꞩ', 'Ɦ' => 'ɦ', 'Ɜ' => 'ɜ', 'Ɡ' => 'ɡ', 'Ɬ' => 'ɬ', 'Ʞ' => 'ʞ', 'Ʇ' => 'ʇ', 'A' => 'a', 'B' => 'b', 'C' => 'c', 'D' => 'd', 'E' => 'e', 'F' => 'f', 'G' => 'g', 'H' => 'h', 'I' => 'i', 'J' => 'j', 'K' => 'k', 'L' => 'l', 'M' => 'm', 'N' => 'n', 'O' => 'o', 'P' => 'p', 'Q' => 'q', 'R' => 'r', 'S' => 's', 'T' => 't', 'U' => 'u', 'V' => 'v', 'W' => 'w', 'X' => 'x', 'Y' => 'y', 'Z' => 'z', '𐐀' => '𐐨', '𐐁' => '𐐩', '𐐂' => '𐐪', '𐐃' => '𐐫', '𐐄' => '𐐬', '𐐅' => '𐐭', '𐐆' => '𐐮', '𐐇' => '𐐯', '𐐈' => '𐐰', '𐐉' => '𐐱', '𐐊' => '𐐲', '𐐋' => '𐐳', '𐐌' => '𐐴', '𐐍' => '𐐵', '𐐎' => '𐐶', '𐐏' => '𐐷', '𐐐' => '𐐸', '𐐑' => '𐐹', '𐐒' => '𐐺', '𐐓' => '𐐻', '𐐔' => '𐐼', '𐐕' => '𐐽', '𐐖' => '𐐾', '𐐗' => '𐐿', '𐐘' => '𐑀', '𐐙' => '𐑁', '𐐚' => '𐑂', '𐐛' => '𐑃', '𐐜' => '𐑄', '𐐝' => '𐑅', '𐐞' => '𐑆', '𐐟' => '𐑇', '𐐠' => '𐑈', '𐐡' => '𐑉', '𐐢' => '𐑊', '𐐣' => '𐑋', '𐐤' => '𐑌', '𐐥' => '𐑍', '𐐦' => '𐑎', '𐐧' => '𐑏', '𑢠' => '𑣀', '𑢡' => '𑣁', '𑢢' => '𑣂', '𑢣' => '𑣃', '𑢤' => '𑣄', '𑢥' => '𑣅', '𑢦' => '𑣆', '𑢧' => '𑣇', '𑢨' => '𑣈', '𑢩' => '𑣉', '𑢪' => '𑣊', '𑢫' => '𑣋', '𑢬' => '𑣌', '𑢭' => '𑣍', '𑢮' => '𑣎', '𑢯' => '𑣏', '𑢰' => '𑣐', '𑢱' => '𑣑', '𑢲' => '𑣒', '𑢳' => '𑣓', '𑢴' => '𑣔', '𑢵' => '𑣕', '𑢶' => '𑣖', '𑢷' => '𑣗', '𑢸' => '𑣘', '𑢹' => '𑣙', '𑢺' => '𑣚', '𑢻' => '𑣛', '𑢼' => '𑣜', '𑢽' => '𑣝', '𑢾' => '𑣞', '𑢿' => '𑣟', ); $result =& $data; unset($data); return $result; 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E', 'f' => 'F', 'g' => 'G', 'h' => 'H', 'i' => 'I', 'j' => 'J', 'k' => 'K', 'l' => 'L', 'm' => 'M', 'n' => 'N', 'o' => 'O', 'p' => 'P', 'q' => 'Q', 'r' => 'R', 's' => 'S', 't' => 'T', 'u' => 'U', 'v' => 'V', 'w' => 'W', 'x' => 'X', 'y' => 'Y', 'z' => 'Z', 'µ' => 'Μ', 'à' => 'À', 'á' => 'Á', 'â' => 'Â', 'ã' => 'Ã', 'ä' => 'Ä', 'å' => 'Å', 'æ' => 'Æ', 'ç' => 'Ç', 'è' => 'È', 'é' => 'É', 'ê' => 'Ê', 'ë' => 'Ë', 'ì' => 'Ì', 'í' => 'Í', 'î' => 'Î', 'ï' => 'Ï', 'ð' => 'Ð', 'ñ' => 'Ñ', 'ò' => 'Ò', 'ó' => 'Ó', 'ô' => 'Ô', 'õ' => 'Õ', 'ö' => 'Ö', 'ø' => 'Ø', 'ù' => 'Ù', 'ú' => 'Ú', 'û' => 'Û', 'ü' => 'Ü', 'ý' => 'Ý', 'þ' => 'Þ', 'ÿ' => 'Ÿ', 'ā' => 'Ā', 'ă' => 'Ă', 'ą' => 'Ą', 'ć' => 'Ć', 'ĉ' => 'Ĉ', 'ċ' => 'Ċ', 'č' => 'Č', 'ď' => 'Ď', 'đ' => 'Đ', 'ē' => 'Ē', 'ĕ' => 'Ĕ', 'ė' => 'Ė', 'ę' => 'Ę', 'ě' => 'Ě', 'ĝ' => 'Ĝ', 'ğ' => 'Ğ', 'ġ' => 'Ġ', 'ģ' => 'Ģ', 'ĥ' => 'Ĥ', 'ħ' => 'Ħ', 'ĩ' => 'Ĩ', 'ī' => 'Ī', 'ĭ' => 'Ĭ', 'į' => 'Į', 'ı' => 'I', 'ij' => 'IJ', 'ĵ' => 'Ĵ', 'ķ' => 'Ķ', 'ĺ' => 'Ĺ', 'ļ' => 'Ļ', 'ľ' => 'Ľ', 'ŀ' => 'Ŀ', 'ł' => 'Ł', 'ń' => 'Ń', 'ņ' => 'Ņ', 'ň' => 'Ň', 'ŋ' => 'Ŋ', 'ō' => 'Ō', 'ŏ' => 'Ŏ', 'ő' => 'Ő', 'œ' => 'Œ', 'ŕ' => 'Ŕ', 'ŗ' => 'Ŗ', 'ř' => 'Ř', 'ś' => 'Ś', 'ŝ' => 'Ŝ', 'ş' => 'Ş', 'š' => 'Š', 'ţ' => 'Ţ', 'ť' => 'Ť', 'ŧ' => 'Ŧ', 'ũ' => 'Ũ', 'ū' => 'Ū', 'ŭ' => 'Ŭ', 'ů' => 'Ů', 'ű' => 'Ű', 'ų' => 'Ų', 'ŵ' => 'Ŵ', 'ŷ' => 'Ŷ', 'ź' => 'Ź', 'ż' => 'Ż', 'ž' => 'Ž', 'ſ' => 'S', 'ƀ' => 'Ƀ', 'ƃ' => 'Ƃ', 'ƅ' => 'Ƅ', 'ƈ' => 'Ƈ', 'ƌ' => 'Ƌ', 'ƒ' => 'Ƒ', 'ƕ' => 'Ƕ', 'ƙ' => 'Ƙ', 'ƚ' => 'Ƚ', 'ƞ' => 'Ƞ', 'ơ' => 'Ơ', 'ƣ' => 'Ƣ', 'ƥ' => 'Ƥ', 'ƨ' => 'Ƨ', 'ƭ' => 'Ƭ', 'ư' => 'Ư', 'ƴ' => 'Ƴ', 'ƶ' => 'Ƶ', 'ƹ' => 'Ƹ', 'ƽ' => 'Ƽ', 'ƿ' => 'Ƿ', 'Dž' => 'DŽ', 'dž' => 'DŽ', 'Lj' => 'LJ', 'lj' => 'LJ', 'Nj' => 'NJ', 'nj' => 'NJ', 'ǎ' => 'Ǎ', 'ǐ' => 'Ǐ', 'ǒ' => 'Ǒ', 'ǔ' => 'Ǔ', 'ǖ' => 'Ǖ', 'ǘ' => 'Ǘ', 'ǚ' => 'Ǚ', 'ǜ' => 'Ǜ', 'ǝ' => 'Ǝ', 'ǟ' => 'Ǟ', 'ǡ' => 'Ǡ', 'ǣ' => 'Ǣ', 'ǥ' => 'Ǥ', 'ǧ' => 'Ǧ', 'ǩ' => 'Ǩ', 'ǫ' => 'Ǫ', 'ǭ' => 'Ǭ', 'ǯ' => 'Ǯ', 'Dz' => 'DZ', 'dz' => 'DZ', 'ǵ' => 'Ǵ', 'ǹ' => 'Ǹ', 'ǻ' => 'Ǻ', 'ǽ' => 'Ǽ', 'ǿ' => 'Ǿ', 'ȁ' => 'Ȁ', 'ȃ' => 'Ȃ', 'ȅ' => 'Ȅ', 'ȇ' => 'Ȇ', 'ȉ' => 'Ȉ', 'ȋ' => 'Ȋ', 'ȍ' => 'Ȍ', 'ȏ' => 'Ȏ', 'ȑ' => 'Ȑ', 'ȓ' => 'Ȓ', 'ȕ' => 'Ȕ', 'ȗ' => 'Ȗ', 'ș' => 'Ș', 'ț' => 'Ț', 'ȝ' => 'Ȝ', 'ȟ' => 'Ȟ', 'ȣ' => 'Ȣ', 'ȥ' => 'Ȥ', 'ȧ' => 'Ȧ', 'ȩ' => 'Ȩ', 'ȫ' => 'Ȫ', 'ȭ' => 'Ȭ', 'ȯ' => 'Ȯ', 'ȱ' => 'Ȱ', 'ȳ' => 'Ȳ', 'ȼ' => 'Ȼ', 'ȿ' => 'Ȿ', 'ɀ' => 'Ɀ', 'ɂ' => 'Ɂ', 'ɇ' => 'Ɇ', 'ɉ' => 'Ɉ', 'ɋ' => 'Ɋ', 'ɍ' => 'Ɍ', 'ɏ' => 'Ɏ', 'ɐ' => 'Ɐ', 'ɑ' => 'Ɑ', 'ɒ' => 'Ɒ', 'ɓ' => 'Ɓ', 'ɔ' => 'Ɔ', 'ɖ' => 'Ɖ', 'ɗ' => 'Ɗ', 'ə' => 'Ə', 'ɛ' => 'Ɛ', 'ɜ' => 'Ɜ', 'ɠ' => 'Ɠ', 'ɡ' => 'Ɡ', 'ɣ' => 'Ɣ', 'ɥ' => 'Ɥ', 'ɦ' => 'Ɦ', 'ɨ' => 'Ɨ', 'ɩ' => 'Ɩ', 'ɫ' => 'Ɫ', 'ɬ' => 'Ɬ', 'ɯ' => 'Ɯ', 'ɱ' => 'Ɱ', 'ɲ' => 'Ɲ', 'ɵ' => 'Ɵ', 'ɽ' => 'Ɽ', 'ʀ' => 'Ʀ', 'ʃ' => 'Ʃ', 'ʇ' => 'Ʇ', 'ʈ' => 'Ʈ', 'ʉ' => 'Ʉ', 'ʊ' => 'Ʊ', 'ʋ' => 'Ʋ', 'ʌ' => 'Ʌ', 'ʒ' => 'Ʒ', 'ʞ' => 'Ʞ', 'ͅ' => 'Ι', 'ͱ' => 'Ͱ', 'ͳ' => 'Ͳ', 'ͷ' => 'Ͷ', 'ͻ' => 'Ͻ', 'ͼ' => 'Ͼ', 'ͽ' => 'Ͽ', 'ά' => 'Ά', 'έ' => 'Έ', 'ή' => 'Ή', 'ί' => 'Ί', 'α' => 'Α', 'β' => 'Β', 'γ' => 'Γ', 'δ' => 'Δ', 'ε' => 'Ε', 'ζ' => 'Ζ', 'η' => 'Η', 'θ' => 'Θ', 'ι' => 'Ι', 'κ' => 'Κ', 'λ' => 'Λ', 'μ' => 'Μ', 'ν' => 'Ν', 'ξ' => 'Ξ', 'ο' => 'Ο', 'π' => 'Π', 'ρ' => 'Ρ', 'ς' => 'Σ', 'σ' => 'Σ', 'τ' => 'Τ', 'υ' => 'Υ', 'φ' => 'Φ', 'χ' => 'Χ', 'ψ' => 'Ψ', 'ω' => 'Ω', 'ϊ' => 'Ϊ', 'ϋ' => 'Ϋ', 'ό' => 'Ό', 'ύ' => 'Ύ', 'ώ' => 'Ώ', 'ϐ' => 'Β', 'ϑ' => 'Θ', 'ϕ' => 'Φ', 'ϖ' => 'Π', 'ϗ' => 'Ϗ', 'ϙ' => 'Ϙ', 'ϛ' => 'Ϛ', 'ϝ' => 'Ϝ', 'ϟ' => 'Ϟ', 'ϡ' => 'Ϡ', 'ϣ' => 'Ϣ', 'ϥ' => 'Ϥ', 'ϧ' => 'Ϧ', 'ϩ' => 'Ϩ', 'ϫ' => 'Ϫ', 'ϭ' => 'Ϭ', 'ϯ' => 'Ϯ', 'ϰ' => 'Κ', 'ϱ' => 'Ρ', 'ϲ' => 'Ϲ', 'ϳ' => 'Ϳ', 'ϵ' => 'Ε', 'ϸ' => 'Ϸ', 'ϻ' => 'Ϻ', 'а' => 'А', 'б' => 'Б', 'в' => 'В', 'г' => 'Г', 'д' => 'Д', 'е' => 'Е', 'ж' => 'Ж', 'з' => 'З', 'и' => 'И', 'й' => 'Й', 'к' => 'К', 'л' => 'Л', 'м' => 'М', 'н' => 'Н', 'о' => 'О', 'п' => 'П', 'р' => 'Р', 'с' => 'С', 'т' => 'Т', 'у' => 'У', 'ф' => 'Ф', 'х' => 'Х', 'ц' => 'Ц', 'ч' => 'Ч', 'ш' => 'Ш', 'щ' => 'Щ', 'ъ' => 'Ъ', 'ы' => 'Ы', 'ь' => 'Ь', 'э' => 'Э', 'ю' => 'Ю', 'я' => 'Я', 'ѐ' => 'Ѐ', 'ё' => 'Ё', 'ђ' => 'Ђ', 'ѓ' => 'Ѓ', 'є' => 'Є', 'ѕ' => 'Ѕ', 'і' => 'І', 'ї' => 'Ї', 'ј' => 'Ј', 'љ' => 'Љ', 'њ' => 'Њ', 'ћ' => 'Ћ', 'ќ' => 'Ќ', 'ѝ' => 'Ѝ', 'ў' => 'Ў', 'џ' => 'Џ', 'ѡ' => 'Ѡ', 'ѣ' => 'Ѣ', 'ѥ' => 'Ѥ', 'ѧ' => 'Ѧ', 'ѩ' => 'Ѩ', 'ѫ' => 'Ѫ', 'ѭ' => 'Ѭ', 'ѯ' => 'Ѯ', 'ѱ' => 'Ѱ', 'ѳ' => 'Ѳ', 'ѵ' => 'Ѵ', 'ѷ' => 'Ѷ', 'ѹ' => 'Ѹ', 'ѻ' => 'Ѻ', 'ѽ' => 'Ѽ', 'ѿ' => 'Ѿ', 'ҁ' => 'Ҁ', 'ҋ' => 'Ҋ', 'ҍ' => 'Ҍ', 'ҏ' => 'Ҏ', 'ґ' => 'Ґ', 'ғ' => 'Ғ', 'ҕ' => 'Ҕ', 'җ' => 'Җ', 'ҙ' => 'Ҙ', 'қ' => 'Қ', 'ҝ' => 'Ҝ', 'ҟ' => 'Ҟ', 'ҡ' => 'Ҡ', 'ң' => 'Ң', 'ҥ' => 'Ҥ', 'ҧ' => 'Ҧ', 'ҩ' => 'Ҩ', 'ҫ' => 'Ҫ', 'ҭ' => 'Ҭ', 'ү' => 'Ү', 'ұ' => 'Ұ', 'ҳ' => 'Ҳ', 'ҵ' => 'Ҵ', 'ҷ' => 'Ҷ', 'ҹ' => 'Ҹ', 'һ' => 'Һ', 'ҽ' => 'Ҽ', 'ҿ' => 'Ҿ', 'ӂ' => 'Ӂ', 'ӄ' => 'Ӄ', 'ӆ' => 'Ӆ', 'ӈ' => 'Ӈ', 'ӊ' => 'Ӊ', 'ӌ' => 'Ӌ', 'ӎ' => 'Ӎ', 'ӏ' => 'Ӏ', 'ӑ' => 'Ӑ', 'ӓ' => 'Ӓ', 'ӕ' => 'Ӕ', 'ӗ' => 'Ӗ', 'ә' => 'Ә', 'ӛ' => 'Ӛ', 'ӝ' => 'Ӝ', 'ӟ' => 'Ӟ', 'ӡ' => 'Ӡ', 'ӣ' => 'Ӣ', 'ӥ' => 'Ӥ', 'ӧ' => 'Ӧ', 'ө' => 'Ө', 'ӫ' => 'Ӫ', 'ӭ' => 'Ӭ', 'ӯ' => 'Ӯ', 'ӱ' => 'Ӱ', 'ӳ' => 'Ӳ', 'ӵ' => 'Ӵ', 'ӷ' => 'Ӷ', 'ӹ' => 'Ӹ', 'ӻ' => 'Ӻ', 'ӽ' => 'Ӽ', 'ӿ' => 'Ӿ', 'ԁ' => 'Ԁ', 'ԃ' => 'Ԃ', 'ԅ' => 'Ԅ', 'ԇ' => 'Ԇ', 'ԉ' => 'Ԉ', 'ԋ' => 'Ԋ', 'ԍ' => 'Ԍ', 'ԏ' => 'Ԏ', 'ԑ' => 'Ԑ', 'ԓ' => 'Ԓ', 'ԕ' => 'Ԕ', 'ԗ' => 'Ԗ', 'ԙ' => 'Ԙ', 'ԛ' => 'Ԛ', 'ԝ' => 'Ԝ', 'ԟ' => 'Ԟ', 'ԡ' => 'Ԡ', 'ԣ' => 'Ԣ', 'ԥ' => 'Ԥ', 'ԧ' => 'Ԧ', 'ԩ' => 'Ԩ', 'ԫ' => 'Ԫ', 'ԭ' => 'Ԭ', 'ԯ' => 'Ԯ', 'ա' => 'Ա', 'բ' => 'Բ', 'գ' => 'Գ', 'դ' => 'Դ', 'ե' => 'Ե', 'զ' => 'Զ', 'է' => 'Է', 'ը' => 'Ը', 'թ' => 'Թ', 'ժ' => 'Ժ', 'ի' => 'Ի', 'լ' => 'Լ', 'խ' => 'Խ', 'ծ' => 'Ծ', 'կ' => 'Կ', 'հ' => 'Հ', 'ձ' => 'Ձ', 'ղ' => 'Ղ', 'ճ' => 'Ճ', 'մ' => 'Մ', 'յ' => 'Յ', 'ն' => 'Ն', 'շ' => 'Շ', 'ո' => 'Ո', 'չ' => 'Չ', 'պ' => 'Պ', 'ջ' => 'Ջ', 'ռ' => 'Ռ', 'ս' => 'Ս', 'վ' => 'Վ', 'տ' => 'Տ', 'ր' => 'Ր', 'ց' => 'Ց', 'ւ' => 'Ւ', 'փ' => 'Փ', 'ք' => 'Ք', 'օ' => 'Օ', 'ֆ' => 'Ֆ', 'ᵹ' => 'Ᵹ', 'ᵽ' => 'Ᵽ', 'ḁ' => 'Ḁ', 'ḃ' => 'Ḃ', 'ḅ' => 'Ḅ', 'ḇ' => 'Ḇ', 'ḉ' => 'Ḉ', 'ḋ' => 'Ḋ', 'ḍ' => 'Ḍ', 'ḏ' => 'Ḏ', 'ḑ' => 'Ḑ', 'ḓ' => 'Ḓ', 'ḕ' => 'Ḕ', 'ḗ' => 'Ḗ', 'ḙ' => 'Ḙ', 'ḛ' => 'Ḛ', 'ḝ' => 'Ḝ', 'ḟ' => 'Ḟ', 'ḡ' => 'Ḡ', 'ḣ' => 'Ḣ', 'ḥ' => 'Ḥ', 'ḧ' => 'Ḧ', 'ḩ' => 'Ḩ', 'ḫ' => 'Ḫ', 'ḭ' => 'Ḭ', 'ḯ' => 'Ḯ', 'ḱ' => 'Ḱ', 'ḳ' => 'Ḳ', 'ḵ' => 'Ḵ', 'ḷ' => 'Ḷ', 'ḹ' => 'Ḹ', 'ḻ' => 'Ḻ', 'ḽ' => 'Ḽ', 'ḿ' => 'Ḿ', 'ṁ' => 'Ṁ', 'ṃ' => 'Ṃ', 'ṅ' => 'Ṅ', 'ṇ' => 'Ṇ', 'ṉ' => 'Ṉ', 'ṋ' => 'Ṋ', 'ṍ' => 'Ṍ', 'ṏ' => 'Ṏ', 'ṑ' => 'Ṑ', 'ṓ' => 'Ṓ', 'ṕ' => 'Ṕ', 'ṗ' => 'Ṗ', 'ṙ' => 'Ṙ', 'ṛ' => 'Ṛ', 'ṝ' => 'Ṝ', 'ṟ' => 'Ṟ', 'ṡ' => 'Ṡ', 'ṣ' => 'Ṣ', 'ṥ' => 'Ṥ', 'ṧ' => 'Ṧ', 'ṩ' => 'Ṩ', 'ṫ' => 'Ṫ', 'ṭ' => 'Ṭ', 'ṯ' => 'Ṯ', 'ṱ' => 'Ṱ', 'ṳ' => 'Ṳ', 'ṵ' => 'Ṵ', 'ṷ' => 'Ṷ', 'ṹ' => 'Ṹ', 'ṻ' => 'Ṻ', 'ṽ' => 'Ṽ', 'ṿ' => 'Ṿ', 'ẁ' => 'Ẁ', 'ẃ' => 'Ẃ', 'ẅ' => 'Ẅ', 'ẇ' => 'Ẇ', 'ẉ' => 'Ẉ', 'ẋ' => 'Ẋ', 'ẍ' => 'Ẍ', 'ẏ' => 'Ẏ', 'ẑ' => 'Ẑ', 'ẓ' => 'Ẓ', 'ẕ' => 'Ẕ', 'ẛ' => 'Ṡ', 'ạ' => 'Ạ', 'ả' => 'Ả', 'ấ' => 'Ấ', 'ầ' => 'Ầ', 'ẩ' => 'Ẩ', 'ẫ' => 'Ẫ', 'ậ' => 'Ậ', 'ắ' => 'Ắ', 'ằ' => 'Ằ', 'ẳ' => 'Ẳ', 'ẵ' => 'Ẵ', 'ặ' => 'Ặ', 'ẹ' => 'Ẹ', 'ẻ' => 'Ẻ', 'ẽ' => 'Ẽ', 'ế' => 'Ế', 'ề' => 'Ề', 'ể' => 'Ể', 'ễ' => 'Ễ', 'ệ' => 'Ệ', 'ỉ' => 'Ỉ', 'ị' => 'Ị', 'ọ' => 'Ọ', 'ỏ' => 'Ỏ', 'ố' => 'Ố', 'ồ' => 'Ồ', 'ổ' => 'Ổ', 'ỗ' => 'Ỗ', 'ộ' => 'Ộ', 'ớ' => 'Ớ', 'ờ' => 'Ờ', 'ở' => 'Ở', 'ỡ' => 'Ỡ', 'ợ' => 'Ợ', 'ụ' => 'Ụ', 'ủ' => 'Ủ', 'ứ' => 'Ứ', 'ừ' => 'Ừ', 'ử' => 'Ử', 'ữ' => 'Ữ', 'ự' => 'Ự', 'ỳ' => 'Ỳ', 'ỵ' => 'Ỵ', 'ỷ' => 'Ỷ', 'ỹ' => 'Ỹ', 'ỻ' => 'Ỻ', 'ỽ' => 'Ỽ', 'ỿ' => 'Ỿ', 'ἀ' => 'Ἀ', 'ἁ' => 'Ἁ', 'ἂ' => 'Ἂ', 'ἃ' => 'Ἃ', 'ἄ' => 'Ἄ', 'ἅ' => 'Ἅ', 'ἆ' => 'Ἆ', 'ἇ' => 'Ἇ', 'ἐ' => 'Ἐ', 'ἑ' => 'Ἑ', 'ἒ' => 'Ἒ', 'ἓ' => 'Ἓ', 'ἔ' => 'Ἔ', 'ἕ' => 'Ἕ', 'ἠ' => 'Ἠ', 'ἡ' => 'Ἡ', 'ἢ' => 'Ἢ', 'ἣ' => 'Ἣ', 'ἤ' => 'Ἤ', 'ἥ' => 'Ἥ', 'ἦ' => 'Ἦ', 'ἧ' => 'Ἧ', 'ἰ' => 'Ἰ', 'ἱ' => 'Ἱ', 'ἲ' => 'Ἲ', 'ἳ' => 'Ἳ', 'ἴ' => 'Ἴ', 'ἵ' => 'Ἵ', 'ἶ' => 'Ἶ', 'ἷ' => 'Ἷ', 'ὀ' => 'Ὀ', 'ὁ' => 'Ὁ', 'ὂ' => 'Ὂ', 'ὃ' => 'Ὃ', 'ὄ' => 'Ὄ', 'ὅ' => 'Ὅ', 'ὑ' => 'Ὑ', 'ὓ' => 'Ὓ', 'ὕ' => 'Ὕ', 'ὗ' => 'Ὗ', 'ὠ' => 'Ὠ', 'ὡ' => 'Ὡ', 'ὢ' => 'Ὢ', 'ὣ' => 'Ὣ', 'ὤ' => 'Ὤ', 'ὥ' => 'Ὥ', 'ὦ' => 'Ὦ', 'ὧ' => 'Ὧ', 'ὰ' => 'Ὰ', 'ά' => 'Ά', 'ὲ' => 'Ὲ', 'έ' => 'Έ', 'ὴ' => 'Ὴ', 'ή' => 'Ή', 'ὶ' => 'Ὶ', 'ί' => 'Ί', 'ὸ' => 'Ὸ', 'ό' => 'Ό', 'ὺ' => 'Ὺ', 'ύ' => 'Ύ', 'ὼ' => 'Ὼ', 'ώ' => 'Ώ', 'ᾀ' => 'ᾈ', 'ᾁ' => 'ᾉ', 'ᾂ' => 'ᾊ', 'ᾃ' => 'ᾋ', 'ᾄ' => 'ᾌ', 'ᾅ' => 'ᾍ', 'ᾆ' => 'ᾎ', 'ᾇ' => 'ᾏ', 'ᾐ' => 'ᾘ', 'ᾑ' => 'ᾙ', 'ᾒ' => 'ᾚ', 'ᾓ' => 'ᾛ', 'ᾔ' => 'ᾜ', 'ᾕ' => 'ᾝ', 'ᾖ' => 'ᾞ', 'ᾗ' => 'ᾟ', 'ᾠ' => 'ᾨ', 'ᾡ' => 'ᾩ', 'ᾢ' => 'ᾪ', 'ᾣ' => 'ᾫ', 'ᾤ' => 'ᾬ', 'ᾥ' => 'ᾭ', 'ᾦ' => 'ᾮ', 'ᾧ' => 'ᾯ', 'ᾰ' => 'Ᾰ', 'ᾱ' => 'Ᾱ', 'ᾳ' => 'ᾼ', 'ι' => 'Ι', 'ῃ' => 'ῌ', 'ῐ' => 'Ῐ', 'ῑ' => 'Ῑ', 'ῠ' => 'Ῠ', 'ῡ' => 'Ῡ', 'ῥ' => 'Ῥ', 'ῳ' => 'ῼ', 'ⅎ' => 'Ⅎ', 'ⅰ' => 'Ⅰ', 'ⅱ' => 'Ⅱ', 'ⅲ' => 'Ⅲ', 'ⅳ' => 'Ⅳ', 'ⅴ' => 'Ⅴ', 'ⅵ' => 'Ⅵ', 'ⅶ' => 'Ⅶ', 'ⅷ' => 'Ⅷ', 'ⅸ' => 'Ⅸ', 'ⅹ' => 'Ⅹ', 'ⅺ' => 'Ⅺ', 'ⅻ' => 'Ⅻ', 'ⅼ' => 'Ⅼ', 'ⅽ' => 'Ⅽ', 'ⅾ' => 'Ⅾ', 'ⅿ' => 'Ⅿ', 'ↄ' => 'Ↄ', 'ⓐ' => 'Ⓐ', 'ⓑ' => 'Ⓑ', 'ⓒ' => 'Ⓒ', 'ⓓ' => 'Ⓓ', 'ⓔ' => 'Ⓔ', 'ⓕ' => 'Ⓕ', 'ⓖ' => 'Ⓖ', 'ⓗ' => 'Ⓗ', 'ⓘ' => 'Ⓘ', 'ⓙ' => 'Ⓙ', 'ⓚ' => 'Ⓚ', 'ⓛ' => 'Ⓛ', 'ⓜ' => 'Ⓜ', 'ⓝ' => 'Ⓝ', 'ⓞ' => 'Ⓞ', 'ⓟ' => 'Ⓟ', 'ⓠ' => 'Ⓠ', 'ⓡ' => 'Ⓡ', 'ⓢ' => 'Ⓢ', 'ⓣ' => 'Ⓣ', 'ⓤ' => 'Ⓤ', 'ⓥ' => 'Ⓥ', 'ⓦ' => 'Ⓦ', 'ⓧ' => 'Ⓧ', 'ⓨ' => 'Ⓨ', 'ⓩ' => 'Ⓩ', 'ⰰ' => 'Ⰰ', 'ⰱ' => 'Ⰱ', 'ⰲ' => 'Ⰲ', 'ⰳ' => 'Ⰳ', 'ⰴ' => 'Ⰴ', 'ⰵ' => 'Ⰵ', 'ⰶ' => 'Ⰶ', 'ⰷ' => 'Ⰷ', 'ⰸ' => 'Ⰸ', 'ⰹ' => 'Ⰹ', 'ⰺ' => 'Ⰺ', 'ⰻ' => 'Ⰻ', 'ⰼ' => 'Ⰼ', 'ⰽ' => 'Ⰽ', 'ⰾ' => 'Ⰾ', 'ⰿ' => 'Ⰿ', 'ⱀ' => 'Ⱀ', 'ⱁ' => 'Ⱁ', 'ⱂ' => 'Ⱂ', 'ⱃ' => 'Ⱃ', 'ⱄ' => 'Ⱄ', 'ⱅ' => 'Ⱅ', 'ⱆ' => 'Ⱆ', 'ⱇ' => 'Ⱇ', 'ⱈ' => 'Ⱈ', 'ⱉ' => 'Ⱉ', 'ⱊ' => 'Ⱊ', 'ⱋ' => 'Ⱋ', 'ⱌ' => 'Ⱌ', 'ⱍ' => 'Ⱍ', 'ⱎ' => 'Ⱎ', 'ⱏ' => 'Ⱏ', 'ⱐ' => 'Ⱐ', 'ⱑ' => 'Ⱑ', 'ⱒ' => 'Ⱒ', 'ⱓ' => 'Ⱓ', 'ⱔ' => 'Ⱔ', 'ⱕ' => 'Ⱕ', 'ⱖ' => 'Ⱖ', 'ⱗ' => 'Ⱗ', 'ⱘ' => 'Ⱘ', 'ⱙ' => 'Ⱙ', 'ⱚ' => 'Ⱚ', 'ⱛ' => 'Ⱛ', 'ⱜ' => 'Ⱜ', 'ⱝ' => 'Ⱝ', 'ⱞ' => 'Ⱞ', 'ⱡ' => 'Ⱡ', 'ⱥ' => 'Ⱥ', 'ⱦ' => 'Ⱦ', 'ⱨ' => 'Ⱨ', 'ⱪ' => 'Ⱪ', 'ⱬ' => 'Ⱬ', 'ⱳ' => 'Ⱳ', 'ⱶ' => 'Ⱶ', 'ⲁ' => 'Ⲁ', 'ⲃ' => 'Ⲃ', 'ⲅ' => 'Ⲅ', 'ⲇ' => 'Ⲇ', 'ⲉ' => 'Ⲉ', 'ⲋ' => 'Ⲋ', 'ⲍ' => 'Ⲍ', 'ⲏ' => 'Ⲏ', 'ⲑ' => 'Ⲑ', 'ⲓ' => 'Ⲓ', 'ⲕ' => 'Ⲕ', 'ⲗ' => 'Ⲗ', 'ⲙ' => 'Ⲙ', 'ⲛ' => 'Ⲛ', 'ⲝ' => 'Ⲝ', 'ⲟ' => 'Ⲟ', 'ⲡ' => 'Ⲡ', 'ⲣ' => 'Ⲣ', 'ⲥ' => 'Ⲥ', 'ⲧ' => 'Ⲧ', 'ⲩ' => 'Ⲩ', 'ⲫ' => 'Ⲫ', 'ⲭ' => 'Ⲭ', 'ⲯ' => 'Ⲯ', 'ⲱ' => 'Ⲱ', 'ⲳ' => 'Ⲳ', 'ⲵ' => 'Ⲵ', 'ⲷ' => 'Ⲷ', 'ⲹ' => 'Ⲹ', 'ⲻ' => 'Ⲻ', 'ⲽ' => 'Ⲽ', 'ⲿ' => 'Ⲿ', 'ⳁ' => 'Ⳁ', 'ⳃ' => 'Ⳃ', 'ⳅ' => 'Ⳅ', 'ⳇ' => 'Ⳇ', 'ⳉ' => 'Ⳉ', 'ⳋ' => 'Ⳋ', 'ⳍ' => 'Ⳍ', 'ⳏ' => 'Ⳏ', 'ⳑ' => 'Ⳑ', 'ⳓ' => 'Ⳓ', 'ⳕ' => 'Ⳕ', 'ⳗ' => 'Ⳗ', 'ⳙ' => 'Ⳙ', 'ⳛ' => 'Ⳛ', 'ⳝ' => 'Ⳝ', 'ⳟ' => 'Ⳟ', 'ⳡ' => 'Ⳡ', 'ⳣ' => 'Ⳣ', 'ⳬ' => 'Ⳬ', 'ⳮ' => 'Ⳮ', 'ⳳ' => 'Ⳳ', 'ⴀ' => 'Ⴀ', 'ⴁ' => 'Ⴁ', 'ⴂ' => 'Ⴂ', 'ⴃ' => 'Ⴃ', 'ⴄ' => 'Ⴄ', 'ⴅ' => 'Ⴅ', 'ⴆ' => 'Ⴆ', 'ⴇ' => 'Ⴇ', 'ⴈ' => 'Ⴈ', 'ⴉ' => 'Ⴉ', 'ⴊ' => 'Ⴊ', 'ⴋ' => 'Ⴋ', 'ⴌ' => 'Ⴌ', 'ⴍ' => 'Ⴍ', 'ⴎ' => 'Ⴎ', 'ⴏ' => 'Ⴏ', 'ⴐ' => 'Ⴐ', 'ⴑ' => 'Ⴑ', 'ⴒ' => 'Ⴒ', 'ⴓ' => 'Ⴓ', 'ⴔ' => 'Ⴔ', 'ⴕ' => 'Ⴕ', 'ⴖ' => 'Ⴖ', 'ⴗ' => 'Ⴗ', 'ⴘ' => 'Ⴘ', 'ⴙ' => 'Ⴙ', 'ⴚ' => 'Ⴚ', 'ⴛ' => 'Ⴛ', 'ⴜ' => 'Ⴜ', 'ⴝ' => 'Ⴝ', 'ⴞ' => 'Ⴞ', 'ⴟ' => 'Ⴟ', 'ⴠ' => 'Ⴠ', 'ⴡ' => 'Ⴡ', 'ⴢ' => 'Ⴢ', 'ⴣ' => 'Ⴣ', 'ⴤ' => 'Ⴤ', 'ⴥ' => 'Ⴥ', 'ⴧ' => 'Ⴧ', 'ⴭ' => 'Ⴭ', 'ꙁ' => 'Ꙁ', 'ꙃ' => 'Ꙃ', 'ꙅ' => 'Ꙅ', 'ꙇ' => 'Ꙇ', 'ꙉ' => 'Ꙉ', 'ꙋ' => 'Ꙋ', 'ꙍ' => 'Ꙍ', 'ꙏ' => 'Ꙏ', 'ꙑ' => 'Ꙑ', 'ꙓ' => 'Ꙓ', 'ꙕ' => 'Ꙕ', 'ꙗ' => 'Ꙗ', 'ꙙ' => 'Ꙙ', 'ꙛ' => 'Ꙛ', 'ꙝ' => 'Ꙝ', 'ꙟ' => 'Ꙟ', 'ꙡ' => 'Ꙡ', 'ꙣ' => 'Ꙣ', 'ꙥ' => 'Ꙥ', 'ꙧ' => 'Ꙧ', 'ꙩ' => 'Ꙩ', 'ꙫ' => 'Ꙫ', 'ꙭ' => 'Ꙭ', 'ꚁ' => 'Ꚁ', 'ꚃ' => 'Ꚃ', 'ꚅ' => 'Ꚅ', 'ꚇ' => 'Ꚇ', 'ꚉ' => 'Ꚉ', 'ꚋ' => 'Ꚋ', 'ꚍ' => 'Ꚍ', 'ꚏ' => 'Ꚏ', 'ꚑ' => 'Ꚑ', 'ꚓ' => 'Ꚓ', 'ꚕ' => 'Ꚕ', 'ꚗ' => 'Ꚗ', 'ꚙ' => 'Ꚙ', 'ꚛ' => 'Ꚛ', 'ꜣ' => 'Ꜣ', 'ꜥ' => 'Ꜥ', 'ꜧ' => 'Ꜧ', 'ꜩ' => 'Ꜩ', 'ꜫ' => 'Ꜫ', 'ꜭ' => 'Ꜭ', 'ꜯ' => 'Ꜯ', 'ꜳ' => 'Ꜳ', 'ꜵ' => 'Ꜵ', 'ꜷ' => 'Ꜷ', 'ꜹ' => 'Ꜹ', 'ꜻ' => 'Ꜻ', 'ꜽ' => 'Ꜽ', 'ꜿ' => 'Ꜿ', 'ꝁ' => 'Ꝁ', 'ꝃ' => 'Ꝃ', 'ꝅ' => 'Ꝅ', 'ꝇ' => 'Ꝇ', 'ꝉ' => 'Ꝉ', 'ꝋ' => 'Ꝋ', 'ꝍ' => 'Ꝍ', 'ꝏ' => 'Ꝏ', 'ꝑ' => 'Ꝑ', 'ꝓ' => 'Ꝓ', 'ꝕ' => 'Ꝕ', 'ꝗ' => 'Ꝗ', 'ꝙ' => 'Ꝙ', 'ꝛ' => 'Ꝛ', 'ꝝ' => 'Ꝝ', 'ꝟ' => 'Ꝟ', 'ꝡ' => 'Ꝡ', 'ꝣ' => 'Ꝣ', 'ꝥ' => 'Ꝥ', 'ꝧ' => 'Ꝧ', 'ꝩ' => 'Ꝩ', 'ꝫ' => 'Ꝫ', 'ꝭ' => 'Ꝭ', 'ꝯ' => 'Ꝯ', 'ꝺ' => 'Ꝺ', 'ꝼ' => 'Ꝼ', 'ꝿ' => 'Ꝿ', 'ꞁ' => 'Ꞁ', 'ꞃ' => 'Ꞃ', 'ꞅ' => 'Ꞅ', 'ꞇ' => 'Ꞇ', 'ꞌ' => 'Ꞌ', 'ꞑ' => 'Ꞑ', 'ꞓ' => 'Ꞓ', 'ꞗ' => 'Ꞗ', 'ꞙ' => 'Ꞙ', 'ꞛ' => 'Ꞛ', 'ꞝ' => 'Ꞝ', 'ꞟ' => 'Ꞟ', 'ꞡ' => 'Ꞡ', 'ꞣ' => 'Ꞣ', 'ꞥ' => 'Ꞥ', 'ꞧ' => 'Ꞧ', 'ꞩ' => 'Ꞩ', 'a' => 'A', 'b' => 'B', 'c' => 'C', 'd' => 'D', 'e' => 'E', 'f' => 'F', 'g' => 'G', 'h' => 'H', 'i' => 'I', 'j' => 'J', 'k' => 'K', 'l' => 'L', 'm' => 'M', 'n' => 'N', 'o' => 'O', 'p' => 'P', 'q' => 'Q', 'r' => 'R', 's' => 'S', 't' => 'T', 'u' => 'U', 'v' => 'V', 'w' => 'W', 'x' => 'X', 'y' => 'Y', 'z' => 'Z', '𐐨' => '𐐀', '𐐩' => '𐐁', '𐐪' => '𐐂', '𐐫' => '𐐃', '𐐬' => '𐐄', '𐐭' => '𐐅', '𐐮' => '𐐆', '𐐯' => '𐐇', '𐐰' => '𐐈', '𐐱' => '𐐉', '𐐲' => '𐐊', '𐐳' => '𐐋', '𐐴' => '𐐌', '𐐵' => '𐐍', '𐐶' => '𐐎', '𐐷' => '𐐏', '𐐸' => '𐐐', '𐐹' => '𐐑', '𐐺' => '𐐒', '𐐻' => '𐐓', '𐐼' => '𐐔', '𐐽' => '𐐕', '𐐾' => '𐐖', '𐐿' => '𐐗', '𐑀' => '𐐘', '𐑁' => '𐐙', '𐑂' => '𐐚', '𐑃' => '𐐛', '𐑄' => '𐐜', '𐑅' => '𐐝', '𐑆' => '𐐞', '𐑇' => '𐐟', '𐑈' => '𐐠', '𐑉' => '𐐡', '𐑊' => '𐐢', '𐑋' => '𐐣', '𐑌' => '𐐤', '𐑍' => '𐐥', '𐑎' => '𐐦', '𐑏' => '𐐧', '𑣀' => '𑢠', '𑣁' => '𑢡', '𑣂' => '𑢢', '𑣃' => '𑢣', '𑣄' => '𑢤', '𑣅' => '𑢥', '𑣆' => '𑢦', '𑣇' => '𑢧', '𑣈' => '𑢨', '𑣉' => '𑢩', '𑣊' => '𑢪', '𑣋' => '𑢫', '𑣌' => '𑢬', '𑣍' => '𑢭', '𑣎' => '𑢮', '𑣏' => '𑢯', '𑣐' => '𑢰', '𑣑' => '𑢱', '𑣒' => '𑢲', '𑣓' => '𑢳', '𑣔' => '𑢴', '𑣕' => '𑢵', '𑣖' => '𑢶', '𑣗' => '𑢷', '𑣘' => '𑢸', '𑣙' => '𑢹', '𑣚' => '𑢺', '𑣛' => '𑢻', '𑣜' => '𑢼', '𑣝' => '𑢽', '𑣞' => '𑢾', '𑣟' => '𑢿', ); $result =& $data; unset($data); return $result; * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * Marker Interface for the Process Component. * * @author Johannes M. Schmitt */ interface ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * InvalidArgumentException for the Process Component. * * @author Romain Neutron */ class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * LogicException for the Process Component. * * @author Romain Neutron */ class LogicException extends \LogicException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; use Symfony\Component\Process\Process; /** * Exception for failed processes. * * @author Johannes M. Schmitt */ class ProcessFailedException extends RuntimeException { private $process; public function __construct(Process $process) { if ($process->isSuccessful()) { throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); } $error = sprintf('The command "%s" failed.'."\nExit Code: %s(%s)", $process->getCommandLine(), $process->getExitCode(), $process->getExitCodeText() ); if (!$process->isOutputDisabled()) { $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", $process->getOutput(), $process->getErrorOutput() ); } parent::__construct($error); $this->process = $process; } public function getProcess() { return $this->process; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; use Symfony\Component\Process\Process; /** * Exception that is thrown when a process times out. * * @author Johannes M. Schmitt */ class ProcessTimedOutException extends RuntimeException { const TYPE_GENERAL = 1; const TYPE_IDLE = 2; private $process; private $timeoutType; public function __construct(Process $process, $timeoutType) { $this->process = $process; $this->timeoutType = $timeoutType; parent::__construct(sprintf( 'The process "%s" exceeded the timeout of %s seconds.', $process->getCommandLine(), $this->getExceededTimeout() )); } public function getProcess() { return $this->process; } public function isGeneralTimeout() { return $this->timeoutType === self::TYPE_GENERAL; } public function isIdleTimeout() { return $this->timeoutType === self::TYPE_IDLE; } public function getExceededTimeout() { switch ($this->timeoutType) { case self::TYPE_GENERAL: return $this->process->getTimeout(); case self::TYPE_IDLE: return $this->process->getIdleTimeout(); default: throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Exception; /** * RuntimeException for the Process Component. * * @author Johannes M. Schmitt */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; /** * Generic executable finder. * * @author Fabien Potencier * @author Johannes M. Schmitt */ class ExecutableFinder { private $suffixes = array('.exe', '.bat', '.cmd', '.com'); /** * Replaces default suffixes of executable. * * @param array $suffixes */ public function setSuffixes(array $suffixes) { $this->suffixes = $suffixes; } /** * Adds new possible suffix to check for executable. * * @param string $suffix */ public function addSuffix($suffix) { $this->suffixes[] = $suffix; } /** * Finds an executable by name. * * @param string $name The executable name (without the extension) * @param string $default The default to return if no executable is found * @param array $extraDirs Additional dirs to check into * * @return string The executable path or default value */ public function find($name, $default = null, array $extraDirs = array()) { if (ini_get('open_basedir')) { $searchPath = explode(PATH_SEPARATOR, ini_get('open_basedir')); $dirs = array(); foreach ($searchPath as $path) { // Silencing against https://bugs.php.net/69240 if (@is_dir($path)) { $dirs[] = $path; } else { if (basename($path) == $name && @is_executable($path)) { return $path; } } } } else { $dirs = array_merge( explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), $extraDirs ); } $suffixes = array(''); if ('\\' === DIRECTORY_SEPARATOR) { $pathExt = getenv('PATHEXT'); $suffixes = $pathExt ? explode(PATH_SEPARATOR, $pathExt) : $this->suffixes; } foreach ($suffixes as $suffix) { foreach ($dirs as $dir) { if (@is_file($file = $dir.DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === DIRECTORY_SEPARATOR || is_executable($file))) { return $file; } } } return $default; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; /** * An executable finder specifically designed for the PHP executable. * * @author Fabien Potencier * @author Johannes M. Schmitt */ class PhpExecutableFinder { private $executableFinder; public function __construct() { $this->executableFinder = new ExecutableFinder(); } /** * Finds The PHP executable. * * @param bool $includeArgs Whether or not include command arguments * * @return string|false The PHP executable path or false if it cannot be found */ public function find($includeArgs = true) { $args = $this->findArguments(); $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; // HHVM support if (defined('HHVM_VERSION')) { return (getenv('PHP_BINARY') ?: PHP_BINARY).$args; } // PHP_BINARY return the current sapi executable if (defined('PHP_BINARY') && PHP_BINARY && in_array(PHP_SAPI, array('cli', 'cli-server', 'phpdbg')) && is_file(PHP_BINARY)) { return PHP_BINARY.$args; } if ($php = getenv('PHP_PATH')) { if (!is_executable($php)) { return false; } return $php; } if ($php = getenv('PHP_PEAR_PHP_BIN')) { if (is_executable($php)) { return $php; } } $dirs = array(PHP_BINDIR); if ('\\' === DIRECTORY_SEPARATOR) { $dirs[] = 'C:\xampp\php\\'; } return $this->executableFinder->find('php', false, $dirs); } /** * Finds the PHP executable arguments. * * @return array The PHP executable arguments */ public function findArguments() { $arguments = array(); if (defined('HHVM_VERSION')) { $arguments[] = '--php'; } elseif ('phpdbg' === PHP_SAPI) { $arguments[] = '-qrr'; } return $arguments; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\RuntimeException; /** * PhpProcess runs a PHP script in an independent process. * * $p = new PhpProcess(''); * $p->run(); * print $p->getOutput()."\n"; * * @author Fabien Potencier */ class PhpProcess extends Process { /** * Constructor. * * @param string $script The PHP script to run (as a string) * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process * @param int $timeout The timeout in seconds * @param array $options An array of options for proc_open */ public function __construct($script, $cwd = null, array $env = null, $timeout = 60, array $options = array()) { $executableFinder = new PhpExecutableFinder(); if (false === $php = $executableFinder->find()) { $php = null; } if ('phpdbg' === PHP_SAPI) { $file = tempnam(sys_get_temp_dir(), 'dbg'); file_put_contents($file, $script); register_shutdown_function('unlink', $file); $php .= ' '.ProcessUtils::escapeArgument($file); $script = null; } if ('\\' !== DIRECTORY_SEPARATOR && null !== $php) { // exec is mandatory to deal with sending a signal to the process // see https://github.com/symfony/symfony/issues/5030 about prepending // command with exec $php = 'exec '.$php; } parent::__construct($php, $cwd, $env, $script, $timeout, $options); } /** * Sets the path to the PHP binary to use. */ public function setPhpBinary($php) { $this->setCommandLine($php); } /** * {@inheritdoc} */ public function start($callback = null) { if (null === $this->getCommandLine()) { throw new RuntimeException('Unable to find the PHP executable.'); } parent::start($callback); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; /** * @author Romain Neutron * * @internal */ abstract class AbstractPipes implements PipesInterface { /** @var array */ public $pipes = array(); /** @var string */ private $inputBuffer = ''; /** @var resource|null */ private $input; /** @var bool */ private $blocked = true; public function __construct($input) { if (is_resource($input)) { $this->input = $input; } elseif (is_string($input)) { $this->inputBuffer = $input; } else { $this->inputBuffer = (string) $input; } } /** * {@inheritdoc} */ public function close() { foreach ($this->pipes as $pipe) { fclose($pipe); } $this->pipes = array(); } /** * Returns true if a system call has been interrupted. * * @return bool */ protected function hasSystemCallBeenInterrupted() { $lastError = error_get_last(); // stream_select returns false when the `select` system call is interrupted by an incoming signal return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); } /** * Unblocks streams. */ protected function unblock() { if (!$this->blocked) { return; } foreach ($this->pipes as $pipe) { stream_set_blocking($pipe, 0); } if (null !== $this->input) { stream_set_blocking($this->input, 0); } $this->blocked = false; } /** * Writes input to stdin. */ protected function write() { if (!isset($this->pipes[0])) { return; } $input = $this->input; $r = $e = array(); $w = array($this->pipes[0]); // let's have a look if something changed in streams if (false === $n = @stream_select($r, $w, $e, 0, 0)) { return; } foreach ($w as $stdin) { if (isset($this->inputBuffer[0])) { $written = fwrite($stdin, $this->inputBuffer); $this->inputBuffer = substr($this->inputBuffer, $written); if (isset($this->inputBuffer[0])) { return array($this->pipes[0]); } } if ($input) { for (;;) { $data = fread($input, self::CHUNK_SIZE); if (!isset($data[0])) { break; } $written = fwrite($stdin, $data); $data = substr($data, $written); if (isset($data[0])) { $this->inputBuffer = $data; return array($this->pipes[0]); } } if (feof($input)) { // no more data to read on input resource // use an empty buffer in the next reads $this->input = null; } } } // no input to read on resource, buffer is empty if (null === $this->input && !isset($this->inputBuffer[0])) { fclose($this->pipes[0]); unset($this->pipes[0]); } elseif (!$w) { return array($this->pipes[0]); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; /** * PipesInterface manages descriptors and pipes for the use of proc_open. * * @author Romain Neutron * * @internal */ interface PipesInterface { const CHUNK_SIZE = 16384; /** * Returns an array of descriptors for the use of proc_open. * * @return array */ public function getDescriptors(); /** * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. * * @return string[] */ public function getFiles(); /** * Reads data in file handles and pipes. * * @param bool $blocking Whether to use blocking calls or not * @param bool $close Whether to close pipes if they've reached EOF * * @return string[] An array of read data indexed by their fd */ public function readAndWrite($blocking, $close = false); /** * Returns if the current state has open file handles or pipes. * * @return bool */ public function areOpen(); /** * Closes file handles and pipes. */ public function close(); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; use Symfony\Component\Process\Process; /** * UnixPipes implementation uses unix pipes as handles. * * @author Romain Neutron * * @internal */ class UnixPipes extends AbstractPipes { /** @var bool */ private $ttyMode; /** @var bool */ private $ptyMode; /** @var bool */ private $disableOutput; public function __construct($ttyMode, $ptyMode, $input, $disableOutput) { $this->ttyMode = (bool) $ttyMode; $this->ptyMode = (bool) $ptyMode; $this->disableOutput = (bool) $disableOutput; parent::__construct($input); } public function __destruct() { $this->close(); } /** * {@inheritdoc} */ public function getDescriptors() { if ($this->disableOutput) { $nullstream = fopen('/dev/null', 'c'); return array( array('pipe', 'r'), $nullstream, $nullstream, ); } if ($this->ttyMode) { return array( array('file', '/dev/tty', 'r'), array('file', '/dev/tty', 'w'), array('file', '/dev/tty', 'w'), ); } if ($this->ptyMode && Process::isPtySupported()) { return array( array('pty'), array('pty'), array('pty'), ); } return array( array('pipe', 'r'), array('pipe', 'w'), // stdout array('pipe', 'w'), // stderr ); } /** * {@inheritdoc} */ public function getFiles() { return array(); } /** * {@inheritdoc} */ public function readAndWrite($blocking, $close = false) { $this->unblock(); $w = $this->write(); $read = $e = array(); $r = $this->pipes; unset($r[0]); // let's have a look if something changed in streams if (($r || $w) && false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { // if a system call has been interrupted, forget about it, let's try again // otherwise, an error occurred, let's reset pipes if (!$this->hasSystemCallBeenInterrupted()) { $this->pipes = array(); } return $read; } foreach ($r as $pipe) { // prior PHP 5.4 the array passed to stream_select is modified and // lose key association, we have to find back the key $read[$type = array_search($pipe, $this->pipes, true)] = ''; do { $data = fread($pipe, self::CHUNK_SIZE); $read[$type] .= $data; } while (isset($data[0]) && ($close || isset($data[self::CHUNK_SIZE - 1]))); if (!isset($read[$type][0])) { unset($read[$type]); } if ($close && feof($pipe)) { fclose($pipe); unset($this->pipes[$type]); } } return $read; } /** * {@inheritdoc} */ public function areOpen() { return (bool) $this->pipes; } /** * Creates a new UnixPipes instance. * * @param Process $process * @param string|resource $input * * @return UnixPipes */ public static function create(Process $process, $input) { return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled()); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process\Pipes; use Symfony\Component\Process\Process; use Symfony\Component\Process\Exception\RuntimeException; /** * WindowsPipes implementation uses temporary files as handles. * * @see https://bugs.php.net/bug.php?id=51800 * @see https://bugs.php.net/bug.php?id=65650 * * @author Romain Neutron * * @internal */ class WindowsPipes extends AbstractPipes { /** @var array */ private $files = array(); /** @var array */ private $fileHandles = array(); /** @var array */ private $readBytes = array( Process::STDOUT => 0, Process::STDERR => 0, ); /** @var bool */ private $disableOutput; public function __construct($disableOutput, $input) { $this->disableOutput = (bool) $disableOutput; if (!$this->disableOutput) { // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. // Workaround for this problem is to use temporary files instead of pipes on Windows platform. // // @see https://bugs.php.net/bug.php?id=51800 $pipes = array( Process::STDOUT => Process::OUT, Process::STDERR => Process::ERR, ); $tmpCheck = false; $tmpDir = sys_get_temp_dir(); $lastError = 'unknown reason'; set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; }); for ($i = 0;; ++$i) { foreach ($pipes as $pipe => $name) { $file = sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); if (file_exists($file) && !unlink($file)) { continue 2; } $h = fopen($file, 'xb'); if (!$h) { $error = $lastError; if ($tmpCheck || $tmpCheck = unlink(tempnam(false, 'sf_check_'))) { continue; } restore_error_handler(); throw new RuntimeException(sprintf('A temporary file could not be opened to write the process output: %s', $error)); } if (!$h || !$this->fileHandles[$pipe] = fopen($file, 'rb')) { continue 2; } if (isset($this->files[$pipe])) { unlink($this->files[$pipe]); } $this->files[$pipe] = $file; } break; } restore_error_handler(); } parent::__construct($input); } public function __destruct() { $this->close(); $this->removeFiles(); } /** * {@inheritdoc} */ public function getDescriptors() { if ($this->disableOutput) { $nullstream = fopen('NUL', 'c'); return array( array('pipe', 'r'), $nullstream, $nullstream, ); } // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/bug.php?id=51800) // We're not using file handles as it can produce corrupted output https://bugs.php.net/bug.php?id=65650 // So we redirect output within the commandline and pass the nul device to the process return array( array('pipe', 'r'), array('file', 'NUL', 'w'), array('file', 'NUL', 'w'), ); } /** * {@inheritdoc} */ public function getFiles() { return $this->files; } /** * {@inheritdoc} */ public function readAndWrite($blocking, $close = false) { $this->unblock(); $w = $this->write(); $read = $r = $e = array(); if ($blocking) { if ($w) { @stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6); } elseif ($this->fileHandles) { usleep(Process::TIMEOUT_PRECISION * 1E6); } } foreach ($this->fileHandles as $type => $fileHandle) { $data = stream_get_contents($fileHandle, -1, $this->readBytes[$type]); if (isset($data[0])) { $this->readBytes[$type] += strlen($data); $read[$type] = $data; } if ($close) { fclose($fileHandle); unset($this->fileHandles[$type]); } } return $read; } /** * {@inheritdoc} */ public function areOpen() { return $this->pipes && $this->fileHandles; } /** * {@inheritdoc} */ public function close() { parent::close(); foreach ($this->fileHandles as $handle) { fclose($handle); } $this->fileHandles = array(); } /** * Creates a new WindowsPipes instance. * * @param Process $process The process * @param $input * * @return WindowsPipes */ public static function create(Process $process, $input) { return new static($process->isOutputDisabled(), $input); } /** * Removes temporary files. */ private function removeFiles() { foreach ($this->files as $filename) { if (file_exists($filename)) { @unlink($filename); } } $this->files = array(); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Process\Exception\LogicException; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Exception\RuntimeException; use Symfony\Component\Process\Pipes\PipesInterface; use Symfony\Component\Process\Pipes\UnixPipes; use Symfony\Component\Process\Pipes\WindowsPipes; /** * Process is a thin wrapper around proc_* functions to easily * start independent PHP processes. * * @author Fabien Potencier * @author Romain Neutron */ class Process { const ERR = 'err'; const OUT = 'out'; const STATUS_READY = 'ready'; const STATUS_STARTED = 'started'; const STATUS_TERMINATED = 'terminated'; const STDIN = 0; const STDOUT = 1; const STDERR = 2; // Timeout Precision in seconds. const TIMEOUT_PRECISION = 0.2; private $callback; private $commandline; private $cwd; private $env; private $input; private $starttime; private $lastOutputTime; private $timeout; private $idleTimeout; private $options; private $exitcode; private $fallbackStatus = array(); private $processInformation; private $outputDisabled = false; private $stdout; private $stderr; private $enhanceWindowsCompatibility = true; private $enhanceSigchildCompatibility; private $process; private $status = self::STATUS_READY; private $incrementalOutputOffset = 0; private $incrementalErrorOutputOffset = 0; private $tty; private $pty; private $useFileHandles = false; /** @var PipesInterface */ private $processPipes; private $latestSignal; private static $sigchild; /** * Exit codes translation table. * * User-defined errors must use exit codes in the 64-113 range. * * @var array */ public static $exitCodes = array( 0 => 'OK', 1 => 'General error', 2 => 'Misuse of shell builtins', 126 => 'Invoked command cannot execute', 127 => 'Command not found', 128 => 'Invalid exit argument', // signals 129 => 'Hangup', 130 => 'Interrupt', 131 => 'Quit and dump core', 132 => 'Illegal instruction', 133 => 'Trace/breakpoint trap', 134 => 'Process aborted', 135 => 'Bus error: "access to undefined portion of memory object"', 136 => 'Floating point exception: "erroneous arithmetic operation"', 137 => 'Kill (terminate immediately)', 138 => 'User-defined 1', 139 => 'Segmentation violation', 140 => 'User-defined 2', 141 => 'Write to pipe with no one reading', 142 => 'Signal raised by alarm', 143 => 'Termination (request to terminate)', // 144 - not defined 145 => 'Child process terminated, stopped (or continued*)', 146 => 'Continue if stopped', 147 => 'Stop executing temporarily', 148 => 'Terminal stop signal', 149 => 'Background process attempting to read from tty ("in")', 150 => 'Background process attempting to write to tty ("out")', 151 => 'Urgent data available on socket', 152 => 'CPU time limit exceeded', 153 => 'File size limit exceeded', 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', 155 => 'Profiling timer expired', // 156 - not defined 157 => 'Pollable event', // 158 - not defined 159 => 'Bad syscall', ); /** * Constructor. * * @param string $commandline The command line to run * @param string|null $cwd The working directory or null to use the working dir of the current PHP process * @param array|null $env The environment variables or null to use the same environment as the current PHP process * @param string|null $input The input * @param int|float|null $timeout The timeout in seconds or null to disable * @param array $options An array of options for proc_open * * @throws RuntimeException When proc_open is not installed */ public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) { if (!function_exists('proc_open')) { throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); } $this->commandline = $commandline; $this->cwd = $cwd; // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected // @see : https://bugs.php.net/bug.php?id=51800 // @see : https://bugs.php.net/bug.php?id=50524 if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DIRECTORY_SEPARATOR)) { $this->cwd = getcwd(); } if (null !== $env) { $this->setEnv($env); } $this->setInput($input); $this->setTimeout($timeout); $this->useFileHandles = '\\' === DIRECTORY_SEPARATOR; $this->pty = false; $this->enhanceWindowsCompatibility = true; $this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled(); $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options); } public function __destruct() { $this->stop(0); } public function __clone() { $this->resetProcessData(); } /** * Runs the process. * * The callback receives the type of output (out or err) and * some bytes from the output in real-time. It allows to have feedback * from the independent process during execution. * * The STDOUT and STDERR are also available after the process is finished * via the getOutput() and getErrorOutput() methods. * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return int The exit status code * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process stopped after receiving signal * @throws LogicException In case a callback is provided and output has been disabled */ public function run($callback = null) { $this->start($callback); return $this->wait(); } /** * Runs the process. * * This is identical to run() except that an exception is thrown if the process * exits with a non-zero exit code. * * @param callable|null $callback * * @return self * * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled * @throws ProcessFailedException if the process didn't terminate successfully */ public function mustRun($callback = null) { if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); } if (0 !== $this->run($callback)) { throw new ProcessFailedException($this); } return $this; } /** * Starts the process and returns after writing the input to STDIN. * * This method blocks until all STDIN data is sent to the process then it * returns while the process runs in the background. * * The termination of the process can be awaited with wait(). * * The callback receives the type of output (out or err) and some bytes from * the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * @throws LogicException In case a callback is provided and output has been disabled */ public function start($callback = null) { if ($this->isRunning()) { throw new RuntimeException('Process is already running'); } if ($this->outputDisabled && null !== $callback) { throw new LogicException('Output has been disabled, enable it to allow the use of a callback.'); } $this->resetProcessData(); $this->starttime = $this->lastOutputTime = microtime(true); $this->callback = $this->buildCallback($callback); $descriptors = $this->getDescriptors(); $commandline = $this->commandline; if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { $commandline = 'cmd /V:ON /E:ON /D /C "('.$commandline.')'; foreach ($this->processPipes->getFiles() as $offset => $filename) { $commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename); } $commandline .= '"'; if (!isset($this->options['bypass_shell'])) { $this->options['bypass_shell'] = true; } } elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { // last exit code is output on the fourth pipe and caught to work around --enable-sigchild $descriptors[3] = array('pipe', 'w'); // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input $commandline = '{ ('.$this->commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; $commandline .= 'pid=$!; echo $pid >&3; wait $pid; code=$?; echo $code >&3; exit $code'; // Workaround for the bug, when PTS functionality is enabled. // @see : https://bugs.php.net/69442 $ptsWorkaround = fopen(__FILE__, 'r'); } $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options); if (!is_resource($this->process)) { throw new RuntimeException('Unable to launch a new process.'); } $this->status = self::STATUS_STARTED; if (isset($descriptors[3])) { $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]); } if ($this->tty) { return; } $this->updateStatus(false); $this->checkTimeout(); } /** * Restarts the process. * * Be warned that the process is cloned before being started. * * @param callable|null $callback A PHP callback to run whenever there is some * output available on STDOUT or STDERR * * @return Process The new process * * @throws RuntimeException When process can't be launched * @throws RuntimeException When process is already running * * @see start() */ public function restart($callback = null) { if ($this->isRunning()) { throw new RuntimeException('Process is already running'); } $process = clone $this; $process->start($callback); return $process; } /** * Waits for the process to terminate. * * The callback receives the type of output (out or err) and some bytes * from the output in real-time while writing the standard input to the process. * It allows to have feedback from the independent process during execution. * * @param callable|null $callback A valid PHP callback * * @return int The exitcode of the process * * @throws RuntimeException When process timed out * @throws RuntimeException When process stopped after receiving signal * @throws LogicException When process is not yet started */ public function wait($callback = null) { $this->requireProcessIsStarted(__FUNCTION__); $this->updateStatus(false); if (null !== $callback) { $this->callback = $this->buildCallback($callback); } do { $this->checkTimeout(); $running = '\\' === DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); $this->readPipes($running, '\\' !== DIRECTORY_SEPARATOR || !$running); } while ($running); while ($this->isRunning()) { usleep(1000); } if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); } return $this->exitcode; } /** * Returns the Pid (process identifier), if applicable. * * @return int|null The process id if running, null otherwise */ public function getPid() { return $this->isRunning() ? $this->processInformation['pid'] : null; } /** * Sends a POSIX signal to the process. * * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) * * @return Process * * @throws LogicException In case the process is not running * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed * @throws RuntimeException In case of failure */ public function signal($signal) { $this->doSignal($signal, true); return $this; } /** * Disables fetching output and error output from the underlying process. * * @return Process * * @throws RuntimeException In case the process is already running * @throws LogicException if an idle timeout is set */ public function disableOutput() { if ($this->isRunning()) { throw new RuntimeException('Disabling output while the process is running is not possible.'); } if (null !== $this->idleTimeout) { throw new LogicException('Output can not be disabled while an idle timeout is set.'); } $this->outputDisabled = true; return $this; } /** * Enables fetching output and error output from the underlying process. * * @return Process * * @throws RuntimeException In case the process is already running */ public function enableOutput() { if ($this->isRunning()) { throw new RuntimeException('Enabling output while the process is running is not possible.'); } $this->outputDisabled = false; return $this; } /** * Returns true in case the output is disabled, false otherwise. * * @return bool */ public function isOutputDisabled() { return $this->outputDisabled; } /** * Returns the current output of the process (STDOUT). * * @return string The process output * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getOutput() { $this->readPipesForOutput(__FUNCTION__); if (false === $ret = stream_get_contents($this->stdout, -1, 0)) { return ''; } return $ret; } /** * Returns the output incrementally. * * In comparison with the getOutput method which always return the whole * output, this one returns the new output since the last call. * * @return string The process output since the last call * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getIncrementalOutput() { $this->readPipesForOutput(__FUNCTION__); $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); $this->incrementalOutputOffset = ftell($this->stdout); if (false === $latest) { return ''; } return $latest; } /** * Clears the process output. * * @return Process */ public function clearOutput() { ftruncate($this->stdout, 0); fseek($this->stdout, 0); $this->incrementalOutputOffset = 0; return $this; } /** * Returns the current error output of the process (STDERR). * * @return string The process error output * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getErrorOutput() { $this->readPipesForOutput(__FUNCTION__); if (false === $ret = stream_get_contents($this->stderr, -1, 0)) { return ''; } return $ret; } /** * Returns the errorOutput incrementally. * * In comparison with the getErrorOutput method which always return the * whole error output, this one returns the new error output since the last * call. * * @return string The process error output since the last call * * @throws LogicException in case the output has been disabled * @throws LogicException In case the process is not started */ public function getIncrementalErrorOutput() { $this->readPipesForOutput(__FUNCTION__); $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); $this->incrementalErrorOutputOffset = ftell($this->stderr); if (false === $latest) { return ''; } return $latest; } /** * Clears the process output. * * @return Process */ public function clearErrorOutput() { ftruncate($this->stderr, 0); fseek($this->stderr, 0); $this->incrementalErrorOutputOffset = 0; return $this; } /** * Returns the exit code returned by the process. * * @return null|int The exit status code, null if the Process is not terminated * * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled */ public function getExitCode() { if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); } $this->updateStatus(false); return $this->exitcode; } /** * Returns a string representation for the exit code returned by the process. * * This method relies on the Unix exit code status standardization * and might not be relevant for other operating systems. * * @return null|string A string representation for the exit status code, null if the Process is not terminated * * @see http://tldp.org/LDP/abs/html/exitcodes.html * @see http://en.wikipedia.org/wiki/Unix_signal */ public function getExitCodeText() { if (null === $exitcode = $this->getExitCode()) { return; } return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; } /** * Checks if the process ended successfully. * * @return bool true if the process ended successfully, false otherwise */ public function isSuccessful() { return 0 === $this->getExitCode(); } /** * Returns true if the child process has been terminated by an uncaught signal. * * It always returns false on Windows. * * @return bool * * @throws RuntimeException In case --enable-sigchild is activated * @throws LogicException In case the process is not terminated */ public function hasBeenSignaled() { $this->requireProcessIsTerminated(__FUNCTION__); if (!$this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); } return $this->processInformation['signaled']; } /** * Returns the number of the signal that caused the child process to terminate its execution. * * It is only meaningful if hasBeenSignaled() returns true. * * @return int * * @throws RuntimeException In case --enable-sigchild is activated * @throws LogicException In case the process is not terminated */ public function getTermSignal() { $this->requireProcessIsTerminated(__FUNCTION__); if ($this->isSigchildEnabled() && (!$this->enhanceSigchildCompatibility || -1 === $this->processInformation['termsig'])) { throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); } return $this->processInformation['termsig']; } /** * Returns true if the child process has been stopped by a signal. * * It always returns false on Windows. * * @return bool * * @throws LogicException In case the process is not terminated */ public function hasBeenStopped() { $this->requireProcessIsTerminated(__FUNCTION__); return $this->processInformation['stopped']; } /** * Returns the number of the signal that caused the child process to stop its execution. * * It is only meaningful if hasBeenStopped() returns true. * * @return int * * @throws LogicException In case the process is not terminated */ public function getStopSignal() { $this->requireProcessIsTerminated(__FUNCTION__); return $this->processInformation['stopsig']; } /** * Checks if the process is currently running. * * @return bool true if the process is currently running, false otherwise */ public function isRunning() { if (self::STATUS_STARTED !== $this->status) { return false; } $this->updateStatus(false); return $this->processInformation['running']; } /** * Checks if the process has been started with no regard to the current state. * * @return bool true if status is ready, false otherwise */ public function isStarted() { return $this->status != self::STATUS_READY; } /** * Checks if the process is terminated. * * @return bool true if process is terminated, false otherwise */ public function isTerminated() { $this->updateStatus(false); return $this->status == self::STATUS_TERMINATED; } /** * Gets the process status. * * The status is one of: ready, started, terminated. * * @return string The current process status */ public function getStatus() { $this->updateStatus(false); return $this->status; } /** * Stops the process. * * @param int|float $timeout The timeout in seconds * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) * * @return int The exit-code of the process */ public function stop($timeout = 10, $signal = null) { $timeoutMicro = microtime(true) + $timeout; if ($this->isRunning()) { // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here $this->doSignal(15, false); do { usleep(1000); } while ($this->isRunning() && microtime(true) < $timeoutMicro); if ($this->isRunning()) { // Avoid exception here: process is supposed to be running, but it might have stopped just // after this line. In any case, let's silently discard the error, we cannot do anything. $this->doSignal($signal ?: 9, false); } } if ($this->isRunning()) { if (isset($this->fallbackStatus['pid'])) { unset($this->fallbackStatus['pid']); return $this->stop(0, $signal); } $this->close(); } return $this->exitcode; } /** * Adds a line to the STDOUT stream. * * @internal * * @param string $line The line to append */ public function addOutput($line) { $this->lastOutputTime = microtime(true); fseek($this->stdout, 0, SEEK_END); fwrite($this->stdout, $line); fseek($this->stdout, $this->incrementalOutputOffset); } /** * Adds a line to the STDERR stream. * * @internal * * @param string $line The line to append */ public function addErrorOutput($line) { $this->lastOutputTime = microtime(true); fseek($this->stderr, 0, SEEK_END); fwrite($this->stderr, $line); fseek($this->stderr, $this->incrementalErrorOutputOffset); } /** * Gets the command line to be executed. * * @return string The command to execute */ public function getCommandLine() { return $this->commandline; } /** * Sets the command line to be executed. * * @param string $commandline The command to execute * * @return self The current Process instance */ public function setCommandLine($commandline) { $this->commandline = $commandline; return $this; } /** * Gets the process timeout (max. runtime). * * @return float|null The timeout in seconds or null if it's disabled */ public function getTimeout() { return $this->timeout; } /** * Gets the process idle timeout (max. time since last output). * * @return float|null The timeout in seconds or null if it's disabled */ public function getIdleTimeout() { return $this->idleTimeout; } /** * Sets the process timeout (max. runtime). * * To disable the timeout, set this value to null. * * @param int|float|null $timeout The timeout in seconds * * @return self The current Process instance * * @throws InvalidArgumentException if the timeout is negative */ public function setTimeout($timeout) { $this->timeout = $this->validateTimeout($timeout); return $this; } /** * Sets the process idle timeout (max. time since last output). * * To disable the timeout, set this value to null. * * @param int|float|null $timeout The timeout in seconds * * @return self The current Process instance * * @throws LogicException if the output is disabled * @throws InvalidArgumentException if the timeout is negative */ public function setIdleTimeout($timeout) { if (null !== $timeout && $this->outputDisabled) { throw new LogicException('Idle timeout can not be set while the output is disabled.'); } $this->idleTimeout = $this->validateTimeout($timeout); return $this; } /** * Enables or disables the TTY mode. * * @param bool $tty True to enabled and false to disable * * @return self The current Process instance * * @throws RuntimeException In case the TTY mode is not supported */ public function setTty($tty) { if ('\\' === DIRECTORY_SEPARATOR && $tty) { throw new RuntimeException('TTY mode is not supported on Windows platform.'); } if ($tty) { static $isTtySupported; if (null === $isTtySupported) { $isTtySupported = (bool) @proc_open('echo 1 >/dev/null', array(array('file', '/dev/tty', 'r'), array('file', '/dev/tty', 'w'), array('file', '/dev/tty', 'w')), $pipes); } if (!$isTtySupported) { throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.'); } } $this->tty = (bool) $tty; return $this; } /** * Checks if the TTY mode is enabled. * * @return bool true if the TTY mode is enabled, false otherwise */ public function isTty() { return $this->tty; } /** * Sets PTY mode. * * @param bool $bool * * @return self */ public function setPty($bool) { $this->pty = (bool) $bool; return $this; } /** * Returns PTY state. * * @return bool */ public function isPty() { return $this->pty; } /** * Gets the working directory. * * @return string|null The current working directory or null on failure */ public function getWorkingDirectory() { if (null === $this->cwd) { // getcwd() will return false if any one of the parent directories does not have // the readable or search mode set, even if the current directory does return getcwd() ?: null; } return $this->cwd; } /** * Sets the current working directory. * * @param string $cwd The new working directory * * @return self The current Process instance */ public function setWorkingDirectory($cwd) { $this->cwd = $cwd; return $this; } /** * Gets the environment variables. * * @return array The current environment variables */ public function getEnv() { return $this->env; } /** * Sets the environment variables. * * An environment variable value should be a string. * If it is an array, the variable is ignored. * * That happens in PHP when 'argv' is registered into * the $_ENV array for instance. * * @param array $env The new environment variables * * @return self The current Process instance */ public function setEnv(array $env) { // Process can not handle env values that are arrays $env = array_filter($env, function ($value) { return !is_array($value); }); $this->env = array(); foreach ($env as $key => $value) { $this->env[$key] = (string) $value; } return $this; } /** * Gets the contents of STDIN. * * @return string|null The current contents * * @deprecated since version 2.5, to be removed in 3.0. * Use setInput() instead. * This method is deprecated in favor of getInput. */ public function getStdin() { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.5 and will be removed in 3.0. Use the getInput() method instead.', E_USER_DEPRECATED); return $this->getInput(); } /** * Gets the Process input. * * @return null|string The Process input */ public function getInput() { return $this->input; } /** * Sets the contents of STDIN. * * @param string|null $stdin The new contents * * @return self The current Process instance * * @deprecated since version 2.5, to be removed in 3.0. * Use setInput() instead. * * @throws LogicException In case the process is running * @throws InvalidArgumentException In case the argument is invalid */ public function setStdin($stdin) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.5 and will be removed in 3.0. Use the setInput() method instead.', E_USER_DEPRECATED); return $this->setInput($stdin); } /** * Sets the input. * * This content will be passed to the underlying process standard input. * * @param mixed $input The content * * @return self The current Process instance * * @throws LogicException In case the process is running * * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0. */ public function setInput($input) { if ($this->isRunning()) { throw new LogicException('Input can not be set while the process is running.'); } $this->input = ProcessUtils::validateInput(__METHOD__, $input); return $this; } /** * Gets the options for proc_open. * * @return array The current options */ public function getOptions() { return $this->options; } /** * Sets the options for proc_open. * * @param array $options The new options * * @return self The current Process instance */ public function setOptions(array $options) { $this->options = $options; return $this; } /** * Gets whether or not Windows compatibility is enabled. * * This is true by default. * * @return bool */ public function getEnhanceWindowsCompatibility() { return $this->enhanceWindowsCompatibility; } /** * Sets whether or not Windows compatibility is enabled. * * @param bool $enhance * * @return self The current Process instance */ public function setEnhanceWindowsCompatibility($enhance) { $this->enhanceWindowsCompatibility = (bool) $enhance; return $this; } /** * Returns whether sigchild compatibility mode is activated or not. * * @return bool */ public function getEnhanceSigchildCompatibility() { return $this->enhanceSigchildCompatibility; } /** * Activates sigchild compatibility mode. * * Sigchild compatibility mode is required to get the exit code and * determine the success of a process when PHP has been compiled with * the --enable-sigchild option * * @param bool $enhance * * @return self The current Process instance */ public function setEnhanceSigchildCompatibility($enhance) { $this->enhanceSigchildCompatibility = (bool) $enhance; return $this; } /** * Performs a check between the timeout definition and the time the process started. * * In case you run a background process (with the start method), you should * trigger this method regularly to ensure the process timeout * * @throws ProcessTimedOutException In case the timeout was reached */ public function checkTimeout() { if ($this->status !== self::STATUS_STARTED) { return; } if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { $this->stop(0); throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); } if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { $this->stop(0); throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); } } /** * Returns whether PTY is supported on the current operating system. * * @return bool */ public static function isPtySupported() { static $result; if (null !== $result) { return $result; } if ('\\' === DIRECTORY_SEPARATOR) { return $result = false; } return $result = (bool) @proc_open('echo 1 >/dev/null', array(array('pty'), array('pty'), array('pty')), $pipes); } /** * Creates the descriptors needed by the proc_open. * * @return array */ private function getDescriptors() { if ('\\' === DIRECTORY_SEPARATOR) { $this->processPipes = WindowsPipes::create($this, $this->input); } else { $this->processPipes = UnixPipes::create($this, $this->input); } return $this->processPipes->getDescriptors(); } /** * Builds up the callback used by wait(). * * The callbacks adds all occurred output to the specific buffer and calls * the user callback (if present) with the received output. * * @param callable|null $callback The user defined PHP callback * * @return \Closure A PHP closure */ protected function buildCallback($callback) { $that = $this; $out = self::OUT; $callback = function ($type, $data) use ($that, $callback, $out) { if ($out == $type) { $that->addOutput($data); } else { $that->addErrorOutput($data); } if (null !== $callback) { call_user_func($callback, $type, $data); } }; return $callback; } /** * Updates the status of the process, reads pipes. * * @param bool $blocking Whether to use a blocking read call */ protected function updateStatus($blocking) { if (self::STATUS_STARTED !== $this->status) { return; } $this->processInformation = proc_get_status($this->process); $running = $this->processInformation['running']; $this->readPipes($running && $blocking, '\\' !== DIRECTORY_SEPARATOR || !$running); if ($this->fallbackStatus && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { $this->processInformation = $this->fallbackStatus + $this->processInformation; } if (!$running) { $this->close(); } } /** * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. * * @return bool */ protected function isSigchildEnabled() { if (null !== self::$sigchild) { return self::$sigchild; } if (!function_exists('phpinfo') || defined('HHVM_VERSION')) { return self::$sigchild = false; } ob_start(); phpinfo(INFO_GENERAL); return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); } /** * Reads pipes for the freshest output. * * @param $caller The name of the method that needs fresh outputs * * @throws LogicException in case output has been disabled or process is not started */ private function readPipesForOutput($caller) { if ($this->outputDisabled) { throw new LogicException('Output has been disabled.'); } $this->requireProcessIsStarted($caller); $this->updateStatus(false); } /** * Validates and returns the filtered timeout. * * @param int|float|null $timeout * * @return float|null * * @throws InvalidArgumentException if the given timeout is a negative number */ private function validateTimeout($timeout) { $timeout = (float) $timeout; if (0.0 === $timeout) { $timeout = null; } elseif ($timeout < 0) { throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); } return $timeout; } /** * Reads pipes, executes callback. * * @param bool $blocking Whether to use blocking calls or not * @param bool $close Whether to close file handles or not */ private function readPipes($blocking, $close) { $result = $this->processPipes->readAndWrite($blocking, $close); $callback = $this->callback; foreach ($result as $type => $data) { if (3 !== $type) { $callback($type === self::STDOUT ? self::OUT : self::ERR, $data); } elseif (!isset($this->fallbackStatus['signaled'])) { $this->fallbackStatus['exitcode'] = (int) $data; } } } /** * Closes process resource, closes file handles, sets the exitcode. * * @return int The exitcode */ private function close() { $this->processPipes->close(); if (is_resource($this->process)) { proc_close($this->process); } $this->exitcode = $this->processInformation['exitcode']; $this->status = self::STATUS_TERMINATED; if (-1 === $this->exitcode) { if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { // if process has been signaled, no exitcode but a valid termsig, apply Unix convention $this->exitcode = 128 + $this->processInformation['termsig']; } elseif ($this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { $this->processInformation['signaled'] = true; $this->processInformation['termsig'] = -1; } } // Free memory from self-reference callback created by buildCallback // Doing so in other contexts like __destruct or by garbage collector is ineffective // Now pipes are closed, so the callback is no longer necessary $this->callback = null; return $this->exitcode; } /** * Resets data related to the latest run of the process. */ private function resetProcessData() { $this->starttime = null; $this->callback = null; $this->exitcode = null; $this->fallbackStatus = array(); $this->processInformation = null; $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'wb+'); $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'wb+'); $this->process = null; $this->latestSignal = null; $this->status = self::STATUS_READY; $this->incrementalOutputOffset = 0; $this->incrementalErrorOutputOffset = 0; } /** * Sends a POSIX signal to the process. * * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) * @param bool $throwException Whether to throw exception in case signal failed * * @return bool True if the signal was sent successfully, false otherwise * * @throws LogicException In case the process is not running * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed * @throws RuntimeException In case of failure */ private function doSignal($signal, $throwException) { if (null === $pid = $this->getPid()) { if ($throwException) { throw new LogicException('Can not send signal on a non running process.'); } return false; } if ('\\' === DIRECTORY_SEPARATOR) { exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); if ($exitCode && $this->isRunning()) { if ($throwException) { throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); } return false; } } else { if (!$this->enhanceSigchildCompatibility || !$this->isSigchildEnabled()) { $ok = @proc_terminate($this->process, $signal); } elseif (function_exists('posix_kill')) { $ok = @posix_kill($pid, $signal); } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), array(2 => array('pipe', 'w')), $pipes)) { $ok = false === fgets($pipes[2]); } if (!$ok) { if ($throwException) { throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); } return false; } } $this->latestSignal = (int) $signal; $this->fallbackStatus['signaled'] = true; $this->fallbackStatus['exitcode'] = -1; $this->fallbackStatus['termsig'] = $this->latestSignal; return true; } /** * Ensures the process is running or terminated, throws a LogicException if the process has a not started. * * @param string $functionName The function name that was called * * @throws LogicException If the process has not run. */ private function requireProcessIsStarted($functionName) { if (!$this->isStarted()) { throw new LogicException(sprintf('Process must be started before calling %s.', $functionName)); } } /** * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`. * * @param string $functionName The function name that was called * * @throws LogicException If the process is not yet terminated. */ private function requireProcessIsTerminated($functionName) { if (!$this->isTerminated()) { throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; use Symfony\Component\Process\Exception\LogicException; /** * Process builder. * * @author Kris Wallsmith */ class ProcessBuilder { private $arguments; private $cwd; private $env = array(); private $input; private $timeout = 60; private $options = array(); private $inheritEnv = true; private $prefix = array(); private $outputDisabled = false; /** * Constructor. * * @param string[] $arguments An array of arguments */ public function __construct(array $arguments = array()) { $this->arguments = $arguments; } /** * Creates a process builder instance. * * @param string[] $arguments An array of arguments * * @return ProcessBuilder */ public static function create(array $arguments = array()) { return new static($arguments); } /** * Adds an unescaped argument to the command string. * * @param string $argument A command argument * * @return ProcessBuilder */ public function add($argument) { $this->arguments[] = $argument; return $this; } /** * Adds a prefix to the command string. * * The prefix is preserved when resetting arguments. * * @param string|array $prefix A command prefix or an array of command prefixes * * @return ProcessBuilder */ public function setPrefix($prefix) { $this->prefix = is_array($prefix) ? $prefix : array($prefix); return $this; } /** * Sets the arguments of the process. * * Arguments must not be escaped. * Previous arguments are removed. * * @param string[] $arguments * * @return ProcessBuilder */ public function setArguments(array $arguments) { $this->arguments = $arguments; return $this; } /** * Sets the working directory. * * @param null|string $cwd The working directory * * @return ProcessBuilder */ public function setWorkingDirectory($cwd) { $this->cwd = $cwd; return $this; } /** * Sets whether environment variables will be inherited or not. * * @param bool $inheritEnv * * @return ProcessBuilder */ public function inheritEnvironmentVariables($inheritEnv = true) { $this->inheritEnv = $inheritEnv; return $this; } /** * Sets an environment variable. * * Setting a variable overrides its previous value. Use `null` to unset a * defined environment variable. * * @param string $name The variable name * @param null|string $value The variable value * * @return ProcessBuilder */ public function setEnv($name, $value) { $this->env[$name] = $value; return $this; } /** * Adds a set of environment variables. * * Already existing environment variables with the same name will be * overridden by the new values passed to this method. Pass `null` to unset * a variable. * * @param array $variables The variables * * @return ProcessBuilder */ public function addEnvironmentVariables(array $variables) { $this->env = array_replace($this->env, $variables); return $this; } /** * Sets the input of the process. * * @param mixed $input The input as a string * * @return ProcessBuilder * * @throws InvalidArgumentException In case the argument is invalid * * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0. */ public function setInput($input) { $this->input = ProcessUtils::validateInput(__METHOD__, $input); return $this; } /** * Sets the process timeout. * * To disable the timeout, set this value to null. * * @param float|null $timeout * * @return ProcessBuilder * * @throws InvalidArgumentException */ public function setTimeout($timeout) { if (null === $timeout) { $this->timeout = null; return $this; } $timeout = (float) $timeout; if ($timeout < 0) { throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); } $this->timeout = $timeout; return $this; } /** * Adds a proc_open option. * * @param string $name The option name * @param string $value The option value * * @return ProcessBuilder */ public function setOption($name, $value) { $this->options[$name] = $value; return $this; } /** * Disables fetching output and error output from the underlying process. * * @return ProcessBuilder */ public function disableOutput() { $this->outputDisabled = true; return $this; } /** * Enables fetching output and error output from the underlying process. * * @return ProcessBuilder */ public function enableOutput() { $this->outputDisabled = false; return $this; } /** * Creates a Process instance and returns it. * * @return Process * * @throws LogicException In case no arguments have been provided */ public function getProcess() { if (0 === count($this->prefix) && 0 === count($this->arguments)) { throw new LogicException('You must add() command arguments before calling getProcess().'); } $options = $this->options; $arguments = array_merge($this->prefix, $this->arguments); $script = implode(' ', array_map(array(__NAMESPACE__.'\\ProcessUtils', 'escapeArgument'), $arguments)); if ($this->inheritEnv) { // include $_ENV for BC purposes $env = array_replace($_ENV, $_SERVER, $this->env); } else { $env = $this->env; } $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options); if ($this->outputDisabled) { $process->disableOutput(); } return $process; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Process; use Symfony\Component\Process\Exception\InvalidArgumentException; /** * ProcessUtils is a bunch of utility methods. * * This class contains static methods only and is not meant to be instantiated. * * @author Martin Hasoň */ class ProcessUtils { /** * This class should not be instantiated. */ private function __construct() { } /** * Escapes a string to be used as a shell argument. * * @param string $argument The argument that will be escaped * * @return string The escaped argument */ public static function escapeArgument($argument) { //Fix for PHP bug #43784 escapeshellarg removes % from given string //Fix for PHP bug #49446 escapeshellarg doesn't work on Windows //@see https://bugs.php.net/bug.php?id=43784 //@see https://bugs.php.net/bug.php?id=49446 if ('\\' === DIRECTORY_SEPARATOR) { if ('' === $argument) { return escapeshellarg($argument); } $escapedArgument = ''; $quote = false; foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { if ('"' === $part) { $escapedArgument .= '\\"'; } elseif (self::isSurroundedBy($part, '%')) { // Avoid environment variable expansion $escapedArgument .= '^%"'.substr($part, 1, -1).'"^%'; } else { // escape trailing backslash if ('\\' === substr($part, -1)) { $part .= '\\'; } $quote = true; $escapedArgument .= $part; } } if ($quote) { $escapedArgument = '"'.$escapedArgument.'"'; } return $escapedArgument; } return escapeshellarg($argument); } /** * Validates and normalizes a Process input. * * @param string $caller The name of method call that validates the input * @param mixed $input The input to validate * * @return mixed The validated input * * @throws InvalidArgumentException In case the input is not valid * * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0. */ public static function validateInput($caller, $input) { if (null !== $input) { if (is_resource($input)) { return $input; } if (is_string($input)) { return $input; } if (is_scalar($input)) { return (string) $input; } // deprecated as of Symfony 2.5, to be removed in 3.0 if (is_object($input) && method_exists($input, '__toString')) { @trigger_error('Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0.', E_USER_DEPRECATED); return (string) $input; } throw new InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller)); } return $input; } private static function isSurroundedBy($arg, $char) { return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts Amqp related classes to array representation. * * @author Grégoire Pineau */ class AmqpCaster { private static $flags = array( AMQP_DURABLE => 'AMQP_DURABLE', AMQP_PASSIVE => 'AMQP_PASSIVE', AMQP_EXCLUSIVE => 'AMQP_EXCLUSIVE', AMQP_AUTODELETE => 'AMQP_AUTODELETE', AMQP_INTERNAL => 'AMQP_INTERNAL', AMQP_NOLOCAL => 'AMQP_NOLOCAL', AMQP_AUTOACK => 'AMQP_AUTOACK', AMQP_IFEMPTY => 'AMQP_IFEMPTY', AMQP_IFUNUSED => 'AMQP_IFUNUSED', AMQP_MANDATORY => 'AMQP_MANDATORY', AMQP_IMMEDIATE => 'AMQP_IMMEDIATE', AMQP_MULTIPLE => 'AMQP_MULTIPLE', AMQP_NOWAIT => 'AMQP_NOWAIT', AMQP_REQUEUE => 'AMQP_REQUEUE', ); private static $exchangeTypes = array( AMQP_EX_TYPE_DIRECT => 'AMQP_EX_TYPE_DIRECT', AMQP_EX_TYPE_FANOUT => 'AMQP_EX_TYPE_FANOUT', AMQP_EX_TYPE_TOPIC => 'AMQP_EX_TYPE_TOPIC', AMQP_EX_TYPE_HEADERS => 'AMQP_EX_TYPE_HEADERS', ); public static function castConnection(\AMQPConnection $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; // BC layer in the amqp lib if (method_exists($c, 'getReadTimeout')) { $timeout = $c->getReadTimeout(); } else { $timeout = $c->getTimeout(); } $a += array( $prefix.'isConnected' => $c->isConnected(), $prefix.'login' => $c->getLogin(), $prefix.'password' => $c->getPassword(), $prefix.'host' => $c->getHost(), $prefix.'port' => $c->getPort(), $prefix.'vhost' => $c->getVhost(), $prefix.'readTimeout' => $timeout, ); return $a; } public static function castChannel(\AMQPChannel $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += array( $prefix.'isConnected' => $c->isConnected(), $prefix.'channelId' => $c->getChannelId(), $prefix.'prefetchSize' => $c->getPrefetchSize(), $prefix.'prefetchCount' => $c->getPrefetchCount(), $prefix.'connection' => $c->getConnection(), ); return $a; } public static function castQueue(\AMQPQueue $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += array( $prefix.'name' => $c->getName(), $prefix.'flags' => self::extractFlags($c->getFlags()), $prefix.'arguments' => $c->getArguments(), $prefix.'connection' => $c->getConnection(), $prefix.'channel' => $c->getChannel(), ); return $a; } public static function castExchange(\AMQPExchange $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += array( $prefix.'name' => $c->getName(), $prefix.'flags' => self::extractFlags($c->getFlags()), $prefix.'type' => isset(self::$exchangeTypes[$c->getType()]) ? new ConstStub(self::$exchangeTypes[$c->getType()], $c->getType()) : $c->getType(), $prefix.'arguments' => $c->getArguments(), $prefix.'channel' => $c->getChannel(), $prefix.'connection' => $c->getConnection(), ); return $a; } public static function castEnvelope(\AMQPEnvelope $c, array $a, Stub $stub, $isNested, $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; if (!($filter & Caster::EXCLUDE_VERBOSE)) { $a += array($prefix.'body' => $c->getBody()); } $a += array( $prefix.'routingKey' => $c->getRoutingKey(), $prefix.'deliveryTag' => $c->getDeliveryTag(), $prefix.'deliveryMode' => new ConstStub($c->getDeliveryMode().(2 === $c->getDeliveryMode() ? ' (persistent)' : ' (non-persistent)'), $c->getDeliveryMode()), $prefix.'exchangeName' => $c->getExchangeName(), $prefix.'isRedelivery' => $c->isRedelivery(), $prefix.'contentType' => $c->getContentType(), $prefix.'contentEncoding' => $c->getContentEncoding(), $prefix.'type' => $c->getType(), $prefix.'timestamp' => $c->getTimeStamp(), $prefix.'priority' => $c->getPriority(), $prefix.'expiration' => $c->getExpiration(), $prefix.'userId' => $c->getUserId(), $prefix.'appId' => $c->getAppId(), $prefix.'messageId' => $c->getMessageId(), $prefix.'replyTo' => $c->getReplyTo(), $prefix.'correlationId' => $c->getCorrelationId(), $prefix.'headers' => $c->getHeaders(), ); return $a; } private static function extractFlags($flags) { $flagsArray = array(); foreach (self::$flags as $value => $name) { if ($flags & $value) { $flagsArray[] = $name; } } if (!$flagsArray) { $flagsArray = array('AMQP_NOPARAM'); } return new ConstStub(implode('|', $flagsArray), $flags); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; /** * Helper for filtering out properties in casters. * * @author Nicolas Grekas */ class Caster { const EXCLUDE_VERBOSE = 1; const EXCLUDE_VIRTUAL = 2; const EXCLUDE_DYNAMIC = 4; const EXCLUDE_PUBLIC = 8; const EXCLUDE_PROTECTED = 16; const EXCLUDE_PRIVATE = 32; const EXCLUDE_NULL = 64; const EXCLUDE_EMPTY = 128; const EXCLUDE_NOT_IMPORTANT = 256; const EXCLUDE_STRICT = 512; const PREFIX_VIRTUAL = "\0~\0"; const PREFIX_DYNAMIC = "\0+\0"; const PREFIX_PROTECTED = "\0*\0"; /** * Casts objects to arrays and adds the dynamic property prefix. * * @param object $obj The object to cast * @param \ReflectionClass $reflector The class reflector to use for inspecting the object definition * * @return array The array-cast of the object, with prefixed dynamic properties */ public static function castObject($obj, \ReflectionClass $reflector) { if ($reflector->hasMethod('__debugInfo')) { $a = $obj->__debugInfo(); } elseif ($obj instanceof \Closure) { $a = array(); } else { $a = (array) $obj; } if ($a) { $p = array_keys($a); foreach ($p as $i => $k) { if (isset($k[0]) && "\0" !== $k[0] && !$reflector->hasProperty($k)) { $p[$i] = self::PREFIX_DYNAMIC.$k; } elseif (isset($k[16]) && "\0" === $k[16] && 0 === strpos($k, "\0class@anonymous\0")) { $p[$i] = "\0".$reflector->getParentClass().'@anonymous'.strrchr($k, "\0"); } } $a = array_combine($p, $a); } return $a; } /** * Filters out the specified properties. * * By default, a single match in the $filter bit field filters properties out, following an "or" logic. * When EXCLUDE_STRICT is set, an "and" logic is applied: all bits must match for a property to be removed. * * @param array $a The array containing the properties to filter * @param int $filter A bit field of Caster::EXCLUDE_* constants specifying which properties to filter out * @param string[] $listedProperties List of properties to exclude when Caster::EXCLUDE_VERBOSE is set, and to preserve when Caster::EXCLUDE_NOT_IMPORTANT is set * * @return array The filtered array */ public static function filter(array $a, $filter, array $listedProperties = array()) { foreach ($a as $k => $v) { $type = self::EXCLUDE_STRICT & $filter; if (null === $v) { $type |= self::EXCLUDE_NULL & $filter; } if (empty($v)) { $type |= self::EXCLUDE_EMPTY & $filter; } if ((self::EXCLUDE_NOT_IMPORTANT & $filter) && !in_array($k, $listedProperties, true)) { $type |= self::EXCLUDE_NOT_IMPORTANT; } if ((self::EXCLUDE_VERBOSE & $filter) && in_array($k, $listedProperties, true)) { $type |= self::EXCLUDE_VERBOSE; } if (!isset($k[1]) || "\0" !== $k[0]) { $type |= self::EXCLUDE_PUBLIC & $filter; } elseif ('~' === $k[1]) { $type |= self::EXCLUDE_VIRTUAL & $filter; } elseif ('+' === $k[1]) { $type |= self::EXCLUDE_DYNAMIC & $filter; } elseif ('*' === $k[1]) { $type |= self::EXCLUDE_PROTECTED & $filter; } else { $type |= self::EXCLUDE_PRIVATE & $filter; } if ((self::EXCLUDE_STRICT & $filter) ? $type === $filter : $type) { unset($a[$k]); } } return $a; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Represents a PHP constant and its value. * * @author Nicolas Grekas */ class ConstStub extends Stub { public function __construct($name, $value) { $this->class = $name; $this->value = $value; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; /** * Represents a cut array. * * @author Nicolas Grekas */ class CutArrayStub extends CutStub { public $preservedSubset; public function __construct(array $value, array $preservedKeys) { parent::__construct($value); $this->preservedSubset = array_intersect_key($value, array_flip($preservedKeys)); $this->cut -= count($this->preservedSubset); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Represents the main properties of a PHP variable, pre-casted by a caster. * * @author Nicolas Grekas */ class CutStub extends Stub { public function __construct($value) { $this->value = $value; switch (gettype($value)) { case 'object': $this->type = self::TYPE_OBJECT; $this->class = get_class($value); $this->cut = -1; break; case 'array': $this->type = self::TYPE_ARRAY; $this->class = self::ARRAY_ASSOC; $this->cut = $this->value = count($value); break; case 'resource': case 'unknown type': case 'resource (closed)': $this->type = self::TYPE_RESOURCE; $this->handle = (int) $value; if ('Unknown' === $this->class = @get_resource_type($value)) { $this->class = 'Closed'; } $this->cut = -1; break; case 'string': $this->type = self::TYPE_STRING; $this->class = preg_match('//u', $value) ? self::STRING_UTF8 : self::STRING_BINARY; $this->cut = self::STRING_BINARY === $this->class ? strlen($value) : mb_strlen($value, 'UTF-8'); $this->value = ''; break; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Doctrine\Common\Proxy\Proxy as CommonProxy; use Doctrine\ORM\Proxy\Proxy as OrmProxy; use Doctrine\ORM\PersistentCollection; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts Doctrine related classes to array representation. * * @author Nicolas Grekas */ class DoctrineCaster { public static function castCommonProxy(CommonProxy $proxy, array $a, Stub $stub, $isNested) { foreach (array('__cloner__', '__initializer__') as $k) { if (array_key_exists($k, $a)) { unset($a[$k]); ++$stub->cut; } } return $a; } public static function castOrmProxy(OrmProxy $proxy, array $a, Stub $stub, $isNested) { foreach (array('_entityPersister', '_identifier') as $k) { if (array_key_exists($k = "\0Doctrine\\ORM\\Proxy\\Proxy\0".$k, $a)) { unset($a[$k]); ++$stub->cut; } } return $a; } public static function castPersistentCollection(PersistentCollection $coll, array $a, Stub $stub, $isNested) { foreach (array('snapshot', 'association', 'typeClass') as $k) { if (array_key_exists($k = "\0Doctrine\\ORM\\PersistentCollection\0".$k, $a)) { $a[$k] = new CutStub($a[$k]); } } return $a; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts DOM related classes to array representation. * * @author Nicolas Grekas */ class DOMCaster { private static $errorCodes = array( DOM_PHP_ERR => 'DOM_PHP_ERR', DOM_INDEX_SIZE_ERR => 'DOM_INDEX_SIZE_ERR', DOMSTRING_SIZE_ERR => 'DOMSTRING_SIZE_ERR', DOM_HIERARCHY_REQUEST_ERR => 'DOM_HIERARCHY_REQUEST_ERR', DOM_WRONG_DOCUMENT_ERR => 'DOM_WRONG_DOCUMENT_ERR', DOM_INVALID_CHARACTER_ERR => 'DOM_INVALID_CHARACTER_ERR', DOM_NO_DATA_ALLOWED_ERR => 'DOM_NO_DATA_ALLOWED_ERR', DOM_NO_MODIFICATION_ALLOWED_ERR => 'DOM_NO_MODIFICATION_ALLOWED_ERR', DOM_NOT_FOUND_ERR => 'DOM_NOT_FOUND_ERR', DOM_NOT_SUPPORTED_ERR => 'DOM_NOT_SUPPORTED_ERR', DOM_INUSE_ATTRIBUTE_ERR => 'DOM_INUSE_ATTRIBUTE_ERR', DOM_INVALID_STATE_ERR => 'DOM_INVALID_STATE_ERR', DOM_SYNTAX_ERR => 'DOM_SYNTAX_ERR', DOM_INVALID_MODIFICATION_ERR => 'DOM_INVALID_MODIFICATION_ERR', DOM_NAMESPACE_ERR => 'DOM_NAMESPACE_ERR', DOM_INVALID_ACCESS_ERR => 'DOM_INVALID_ACCESS_ERR', DOM_VALIDATION_ERR => 'DOM_VALIDATION_ERR', ); private static $nodeTypes = array( XML_ELEMENT_NODE => 'XML_ELEMENT_NODE', XML_ATTRIBUTE_NODE => 'XML_ATTRIBUTE_NODE', XML_TEXT_NODE => 'XML_TEXT_NODE', XML_CDATA_SECTION_NODE => 'XML_CDATA_SECTION_NODE', XML_ENTITY_REF_NODE => 'XML_ENTITY_REF_NODE', XML_ENTITY_NODE => 'XML_ENTITY_NODE', XML_PI_NODE => 'XML_PI_NODE', XML_COMMENT_NODE => 'XML_COMMENT_NODE', XML_DOCUMENT_NODE => 'XML_DOCUMENT_NODE', XML_DOCUMENT_TYPE_NODE => 'XML_DOCUMENT_TYPE_NODE', XML_DOCUMENT_FRAG_NODE => 'XML_DOCUMENT_FRAG_NODE', XML_NOTATION_NODE => 'XML_NOTATION_NODE', XML_HTML_DOCUMENT_NODE => 'XML_HTML_DOCUMENT_NODE', XML_DTD_NODE => 'XML_DTD_NODE', XML_ELEMENT_DECL_NODE => 'XML_ELEMENT_DECL_NODE', XML_ATTRIBUTE_DECL_NODE => 'XML_ATTRIBUTE_DECL_NODE', XML_ENTITY_DECL_NODE => 'XML_ENTITY_DECL_NODE', XML_NAMESPACE_DECL_NODE => 'XML_NAMESPACE_DECL_NODE', ); public static function castException(\DOMException $e, array $a, Stub $stub, $isNested) { $k = Caster::PREFIX_PROTECTED.'code'; if (isset($a[$k], self::$errorCodes[$a[$k]])) { $a[$k] = new ConstStub(self::$errorCodes[$a[$k]], $a[$k]); } return $a; } public static function castLength($dom, array $a, Stub $stub, $isNested) { $a += array( 'length' => $dom->length, ); return $a; } public static function castImplementation($dom, array $a, Stub $stub, $isNested) { $a += array( Caster::PREFIX_VIRTUAL.'Core' => '1.0', Caster::PREFIX_VIRTUAL.'XML' => '2.0', ); return $a; } public static function castNode(\DOMNode $dom, array $a, Stub $stub, $isNested) { $a += array( 'nodeName' => $dom->nodeName, 'nodeValue' => new CutStub($dom->nodeValue), 'nodeType' => new ConstStub(self::$nodeTypes[$dom->nodeType], $dom->nodeType), 'parentNode' => new CutStub($dom->parentNode), 'childNodes' => $dom->childNodes, 'firstChild' => new CutStub($dom->firstChild), 'lastChild' => new CutStub($dom->lastChild), 'previousSibling' => new CutStub($dom->previousSibling), 'nextSibling' => new CutStub($dom->nextSibling), 'attributes' => $dom->attributes, 'ownerDocument' => new CutStub($dom->ownerDocument), 'namespaceURI' => $dom->namespaceURI, 'prefix' => $dom->prefix, 'localName' => $dom->localName, 'baseURI' => $dom->baseURI, 'textContent' => new CutStub($dom->textContent), ); return $a; } public static function castNameSpaceNode(\DOMNameSpaceNode $dom, array $a, Stub $stub, $isNested) { $a += array( 'nodeName' => $dom->nodeName, 'nodeValue' => new CutStub($dom->nodeValue), 'nodeType' => new ConstStub(self::$nodeTypes[$dom->nodeType], $dom->nodeType), 'prefix' => $dom->prefix, 'localName' => $dom->localName, 'namespaceURI' => $dom->namespaceURI, 'ownerDocument' => new CutStub($dom->ownerDocument), 'parentNode' => new CutStub($dom->parentNode), ); return $a; } public static function castDocument(\DOMDocument $dom, array $a, Stub $stub, $isNested, $filter = 0) { $a += array( 'doctype' => $dom->doctype, 'implementation' => $dom->implementation, 'documentElement' => new CutStub($dom->documentElement), 'actualEncoding' => $dom->actualEncoding, 'encoding' => $dom->encoding, 'xmlEncoding' => $dom->xmlEncoding, 'standalone' => $dom->standalone, 'xmlStandalone' => $dom->xmlStandalone, 'version' => $dom->version, 'xmlVersion' => $dom->xmlVersion, 'strictErrorChecking' => $dom->strictErrorChecking, 'documentURI' => $dom->documentURI, 'config' => $dom->config, 'formatOutput' => $dom->formatOutput, 'validateOnParse' => $dom->validateOnParse, 'resolveExternals' => $dom->resolveExternals, 'preserveWhiteSpace' => $dom->preserveWhiteSpace, 'recover' => $dom->recover, 'substituteEntities' => $dom->substituteEntities, ); if (!($filter & Caster::EXCLUDE_VERBOSE)) { $formatOutput = $dom->formatOutput; $dom->formatOutput = true; $a += array(Caster::PREFIX_VIRTUAL.'xml' => $dom->saveXML()); $dom->formatOutput = $formatOutput; } return $a; } public static function castCharacterData(\DOMCharacterData $dom, array $a, Stub $stub, $isNested) { $a += array( 'data' => $dom->data, 'length' => $dom->length, ); return $a; } public static function castAttr(\DOMAttr $dom, array $a, Stub $stub, $isNested) { $a += array( 'name' => $dom->name, 'specified' => $dom->specified, 'value' => $dom->value, 'ownerElement' => $dom->ownerElement, 'schemaTypeInfo' => $dom->schemaTypeInfo, ); return $a; } public static function castElement(\DOMElement $dom, array $a, Stub $stub, $isNested) { $a += array( 'tagName' => $dom->tagName, 'schemaTypeInfo' => $dom->schemaTypeInfo, ); return $a; } public static function castText(\DOMText $dom, array $a, Stub $stub, $isNested) { $a += array( 'wholeText' => $dom->wholeText, ); return $a; } public static function castTypeinfo(\DOMTypeinfo $dom, array $a, Stub $stub, $isNested) { $a += array( 'typeName' => $dom->typeName, 'typeNamespace' => $dom->typeNamespace, ); return $a; } public static function castDomError(\DOMDomError $dom, array $a, Stub $stub, $isNested) { $a += array( 'severity' => $dom->severity, 'message' => $dom->message, 'type' => $dom->type, 'relatedException' => $dom->relatedException, 'related_data' => $dom->related_data, 'location' => $dom->location, ); return $a; } public static function castLocator(\DOMLocator $dom, array $a, Stub $stub, $isNested) { $a += array( 'lineNumber' => $dom->lineNumber, 'columnNumber' => $dom->columnNumber, 'offset' => $dom->offset, 'relatedNode' => $dom->relatedNode, 'uri' => $dom->uri, ); return $a; } public static function castDocumentType(\DOMDocumentType $dom, array $a, Stub $stub, $isNested) { $a += array( 'name' => $dom->name, 'entities' => $dom->entities, 'notations' => $dom->notations, 'publicId' => $dom->publicId, 'systemId' => $dom->systemId, 'internalSubset' => $dom->internalSubset, ); return $a; } public static function castNotation(\DOMNotation $dom, array $a, Stub $stub, $isNested) { $a += array( 'publicId' => $dom->publicId, 'systemId' => $dom->systemId, ); return $a; } public static function castEntity(\DOMEntity $dom, array $a, Stub $stub, $isNested) { $a += array( 'publicId' => $dom->publicId, 'systemId' => $dom->systemId, 'notationName' => $dom->notationName, 'actualEncoding' => $dom->actualEncoding, 'encoding' => $dom->encoding, 'version' => $dom->version, ); return $a; } public static function castProcessingInstruction(\DOMProcessingInstruction $dom, array $a, Stub $stub, $isNested) { $a += array( 'target' => $dom->target, 'data' => $dom->data, ); return $a; } public static function castXPath(\DOMXPath $dom, array $a, Stub $stub, $isNested) { $a += array( 'document' => $dom->document, ); return $a; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Represents an enumeration of values. * * @author Nicolas Grekas */ class EnumStub extends Stub { public function __construct(array $values) { $this->value = $values; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Exception\ThrowingCasterException; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts common Exception classes to array representation. * * @author Nicolas Grekas */ class ExceptionCaster { public static $srcContext = 1; public static $traceArgs = true; public static $errorTypes = array( E_DEPRECATED => 'E_DEPRECATED', E_USER_DEPRECATED => 'E_USER_DEPRECATED', E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR', E_ERROR => 'E_ERROR', E_WARNING => 'E_WARNING', E_PARSE => 'E_PARSE', E_NOTICE => 'E_NOTICE', E_CORE_ERROR => 'E_CORE_ERROR', E_CORE_WARNING => 'E_CORE_WARNING', E_COMPILE_ERROR => 'E_COMPILE_ERROR', E_COMPILE_WARNING => 'E_COMPILE_WARNING', E_USER_ERROR => 'E_USER_ERROR', E_USER_WARNING => 'E_USER_WARNING', E_USER_NOTICE => 'E_USER_NOTICE', E_STRICT => 'E_STRICT', ); public static function castError(\Error $e, array $a, Stub $stub, $isNested, $filter = 0) { return self::filterExceptionArray($stub->class, $a, "\0Error\0", $filter); } public static function castException(\Exception $e, array $a, Stub $stub, $isNested, $filter = 0) { return self::filterExceptionArray($stub->class, $a, "\0Exception\0", $filter); } public static function castErrorException(\ErrorException $e, array $a, Stub $stub, $isNested) { if (isset($a[$s = Caster::PREFIX_PROTECTED.'severity'], self::$errorTypes[$a[$s]])) { $a[$s] = new ConstStub(self::$errorTypes[$a[$s]], $a[$s]); } return $a; } public static function castThrowingCasterException(ThrowingCasterException $e, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_PROTECTED; $xPrefix = "\0Exception\0"; if (isset($a[$xPrefix.'previous'], $a[$xPrefix.'trace'])) { $b = (array) $a[$xPrefix.'previous']; array_unshift($b[$xPrefix.'trace'], array( 'function' => 'new '.get_class($a[$xPrefix.'previous']), 'file' => $b[$prefix.'file'], 'line' => $b[$prefix.'line'], )); $a[$xPrefix.'trace'] = new TraceStub($b[$xPrefix.'trace'], false, 0, -1 - count($a[$xPrefix.'trace']->value)); } unset($a[$xPrefix.'previous'], $a[$prefix.'code'], $a[$prefix.'file'], $a[$prefix.'line']); return $a; } public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, $isNested) { if (!$isNested) { return $a; } $stub->class = ''; $stub->handle = 0; $frames = $trace->value; $a = array(); $j = count($frames); if (0 > $i = $trace->sliceOffset) { $i = max(0, $j + $i); } if (!isset($trace->value[$i])) { return array(); } $lastCall = isset($frames[$i]['function']) ? ' ==> '.(isset($frames[$i]['class']) ? $frames[0]['class'].$frames[$i]['type'] : '').$frames[$i]['function'].'()' : ''; for ($j += $trace->numberingOffset - $i++; isset($frames[$i]); ++$i, --$j) { $call = isset($frames[$i]['function']) ? (isset($frames[$i]['class']) ? $frames[$i]['class'].$frames[$i]['type'] : '').$frames[$i]['function'].'()' : '???'; $a[Caster::PREFIX_VIRTUAL.$j.'. '.$call.$lastCall] = new FrameStub( array( 'object' => isset($frames[$i]['object']) ? $frames[$i]['object'] : null, 'class' => isset($frames[$i]['class']) ? $frames[$i]['class'] : null, 'type' => isset($frames[$i]['type']) ? $frames[$i]['type'] : null, 'function' => isset($frames[$i]['function']) ? $frames[$i]['function'] : null, ) + $frames[$i - 1], $trace->keepArgs, true ); $lastCall = ' ==> '.$call; } $a[Caster::PREFIX_VIRTUAL.$j.'. {main}'.$lastCall] = new FrameStub( array( 'object' => null, 'class' => null, 'type' => null, 'function' => '{main}', ) + $frames[$i - 1], $trace->keepArgs, true ); if (null !== $trace->sliceLength) { $a = array_slice($a, 0, $trace->sliceLength, true); } return $a; } public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, $isNested) { if (!$isNested) { return $a; } $f = $frame->value; $prefix = Caster::PREFIX_VIRTUAL; if (isset($f['file'], $f['line'])) { if (preg_match('/\((\d+)\)(?:\([\da-f]{32}\))? : (?:eval\(\)\'d code|runtime-created function)$/', $f['file'], $match)) { $f['file'] = substr($f['file'], 0, -strlen($match[0])); $f['line'] = (int) $match[1]; } if (file_exists($f['file']) && 0 <= self::$srcContext) { $src[$f['file'].':'.$f['line']] = self::extractSource(explode("\n", file_get_contents($f['file'])), $f['line'], self::$srcContext); if (!empty($f['class']) && is_subclass_of($f['class'], 'Twig_Template') && method_exists($f['class'], 'getDebugInfo')) { $template = isset($f['object']) ? $f['object'] : unserialize(sprintf('O:%d:"%s":0:{}', strlen($f['class']), $f['class'])); $templateName = $template->getTemplateName(); $templateSrc = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getCode() : (method_exists($template, 'getSource') ? $template->getSource() : ''); $templateInfo = $template->getDebugInfo(); if (isset($templateInfo[$f['line']])) { if (method_exists($template, 'getSourceContext')) { $templateName = $template->getSourceContext()->getPath() ?: $templateName; } if ($templateSrc) { $templateSrc = explode("\n", $templateSrc); $src[$templateName.':'.$templateInfo[$f['line']]] = self::extractSource($templateSrc, $templateInfo[$f['line']], self::$srcContext); } else { $src[$templateName] = $templateInfo[$f['line']]; } } } } else { $src[$f['file']] = $f['line']; } $a[$prefix.'src'] = new EnumStub($src); } unset($a[$prefix.'args'], $a[$prefix.'line'], $a[$prefix.'file']); if ($frame->inTraceStub) { unset($a[$prefix.'class'], $a[$prefix.'type'], $a[$prefix.'function']); } foreach ($a as $k => $v) { if (!$v) { unset($a[$k]); } } if ($frame->keepArgs && isset($f['args'])) { $a[$prefix.'args'] = $f['args']; } return $a; } /** * @deprecated since 2.8, to be removed in 3.0. Use the castTraceStub method instead. */ public static function filterTrace(&$trace, $dumpArgs, $offset = 0) { @trigger_error('The '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Use the castTraceStub method instead.', E_USER_DEPRECATED); if (0 > $offset || empty($trace[$offset])) { return $trace = null; } $t = $trace[$offset]; if (empty($t['class']) && isset($t['function'])) { if ('user_error' === $t['function'] || 'trigger_error' === $t['function']) { ++$offset; } } if ($offset) { array_splice($trace, 0, $offset); } foreach ($trace as &$t) { $t = array( 'call' => (isset($t['class']) ? $t['class'].$t['type'] : '').$t['function'].'()', 'file' => isset($t['line']) ? "{$t['file']}:{$t['line']}" : '', 'args' => &$t['args'], ); if (!isset($t['args']) || !$dumpArgs) { unset($t['args']); } } } private static function filterExceptionArray($xClass, array $a, $xPrefix, $filter) { if (isset($a[$xPrefix.'trace'])) { $trace = $a[$xPrefix.'trace']; unset($a[$xPrefix.'trace']); // Ensures the trace is always last } else { $trace = array(); } if (!($filter & Caster::EXCLUDE_VERBOSE)) { array_unshift($trace, array( 'function' => $xClass ? 'new '.$xClass : null, 'file' => $a[Caster::PREFIX_PROTECTED.'file'], 'line' => $a[Caster::PREFIX_PROTECTED.'line'], )); $a[$xPrefix.'trace'] = new TraceStub($trace, self::$traceArgs); } if (empty($a[$xPrefix.'previous'])) { unset($a[$xPrefix.'previous']); } unset($a[$xPrefix.'string'], $a[Caster::PREFIX_DYNAMIC.'xdebug_message'], $a[Caster::PREFIX_DYNAMIC.'__destructorException']); return $a; } private static function extractSource(array $srcArray, $line, $srcContext) { $src = array(); for ($i = $line - 1 - $srcContext; $i <= $line - 1 + $srcContext; ++$i) { $src[] = (isset($srcArray[$i]) ? $srcArray[$i] : '')."\n"; } $ltrim = 0; do { $pad = null; for ($i = $srcContext << 1; $i >= 0; --$i) { if (isset($src[$i][$ltrim]) && "\r" !== ($c = $src[$i][$ltrim]) && "\n" !== $c) { if (null === $pad) { $pad = $c; } if ((' ' !== $c && "\t" !== $c) || $pad !== $c) { break; } } } ++$ltrim; } while (0 > $i && null !== $pad); if (--$ltrim) { foreach ($src as $i => $line) { $src[$i] = isset($line[$ltrim]) && "\r" !== $line[$ltrim] ? substr($line, $ltrim) : ltrim($line, " \t"); } } return implode('', $src); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; /** * Represents a single backtrace frame as returned by debug_backtrace() or Exception->getTrace(). * * @author Nicolas Grekas */ class FrameStub extends EnumStub { public $keepArgs; public $inTraceStub; public function __construct(array $frame, $keepArgs = true, $inTraceStub = false) { $this->value = $frame; $this->keepArgs = $keepArgs; $this->inTraceStub = $inTraceStub; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts classes from the MongoDb extension to array representation. * * @author Nicolas Grekas */ class MongoCaster { public static function castCursor(\MongoCursorInterface $cursor, array $a, Stub $stub, $isNested) { if ($info = $cursor->info()) { foreach ($info as $k => $v) { $a[Caster::PREFIX_VIRTUAL.$k] = $v; } } $a[Caster::PREFIX_VIRTUAL.'dead'] = $cursor->dead(); return $a; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts PDO related classes to array representation. * * @author Nicolas Grekas */ class PdoCaster { private static $pdoAttributes = array( 'CASE' => array( \PDO::CASE_LOWER => 'LOWER', \PDO::CASE_NATURAL => 'NATURAL', \PDO::CASE_UPPER => 'UPPER', ), 'ERRMODE' => array( \PDO::ERRMODE_SILENT => 'SILENT', \PDO::ERRMODE_WARNING => 'WARNING', \PDO::ERRMODE_EXCEPTION => 'EXCEPTION', ), 'TIMEOUT', 'PREFETCH', 'AUTOCOMMIT', 'PERSISTENT', 'DRIVER_NAME', 'SERVER_INFO', 'ORACLE_NULLS' => array( \PDO::NULL_NATURAL => 'NATURAL', \PDO::NULL_EMPTY_STRING => 'EMPTY_STRING', \PDO::NULL_TO_STRING => 'TO_STRING', ), 'CLIENT_VERSION', 'SERVER_VERSION', 'STATEMENT_CLASS', 'EMULATE_PREPARES', 'CONNECTION_STATUS', 'STRINGIFY_FETCHES', 'DEFAULT_FETCH_MODE' => array( \PDO::FETCH_ASSOC => 'ASSOC', \PDO::FETCH_BOTH => 'BOTH', \PDO::FETCH_LAZY => 'LAZY', \PDO::FETCH_NUM => 'NUM', \PDO::FETCH_OBJ => 'OBJ', ), ); public static function castPdo(\PDO $c, array $a, Stub $stub, $isNested) { $attr = array(); $errmode = $c->getAttribute(\PDO::ATTR_ERRMODE); $c->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); foreach (self::$pdoAttributes as $k => $v) { if (!isset($k[0])) { $k = $v; $v = array(); } try { $attr[$k] = 'ERRMODE' === $k ? $errmode : $c->getAttribute(constant('PDO::ATTR_'.$k)); if ($v && isset($v[$attr[$k]])) { $attr[$k] = new ConstStub($v[$attr[$k]], $attr[$k]); } } catch (\Exception $e) { } } $prefix = Caster::PREFIX_VIRTUAL; $a += array( $prefix.'inTransaction' => method_exists($c, 'inTransaction'), $prefix.'errorInfo' => $c->errorInfo(), $prefix.'attributes' => new EnumStub($attr), ); if ($a[$prefix.'inTransaction']) { $a[$prefix.'inTransaction'] = $c->inTransaction(); } else { unset($a[$prefix.'inTransaction']); } if (!isset($a[$prefix.'errorInfo'][1], $a[$prefix.'errorInfo'][2])) { unset($a[$prefix.'errorInfo']); } $c->setAttribute(\PDO::ATTR_ERRMODE, $errmode); return $a; } public static function castPdoStatement(\PDOStatement $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a[$prefix.'errorInfo'] = $c->errorInfo(); if (!isset($a[$prefix.'errorInfo'][1], $a[$prefix.'errorInfo'][2])) { unset($a[$prefix.'errorInfo']); } return $a; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts pqsql resources to array representation. * * @author Nicolas Grekas */ class PgSqlCaster { private static $paramCodes = array( 'server_encoding', 'client_encoding', 'is_superuser', 'session_authorization', 'DateStyle', 'TimeZone', 'IntervalStyle', 'integer_datetimes', 'application_name', 'standard_conforming_strings', ); private static $transactionStatus = array( PGSQL_TRANSACTION_IDLE => 'PGSQL_TRANSACTION_IDLE', PGSQL_TRANSACTION_ACTIVE => 'PGSQL_TRANSACTION_ACTIVE', PGSQL_TRANSACTION_INTRANS => 'PGSQL_TRANSACTION_INTRANS', PGSQL_TRANSACTION_INERROR => 'PGSQL_TRANSACTION_INERROR', PGSQL_TRANSACTION_UNKNOWN => 'PGSQL_TRANSACTION_UNKNOWN', ); private static $resultStatus = array( PGSQL_EMPTY_QUERY => 'PGSQL_EMPTY_QUERY', PGSQL_COMMAND_OK => 'PGSQL_COMMAND_OK', PGSQL_TUPLES_OK => 'PGSQL_TUPLES_OK', PGSQL_COPY_OUT => 'PGSQL_COPY_OUT', PGSQL_COPY_IN => 'PGSQL_COPY_IN', PGSQL_BAD_RESPONSE => 'PGSQL_BAD_RESPONSE', PGSQL_NONFATAL_ERROR => 'PGSQL_NONFATAL_ERROR', PGSQL_FATAL_ERROR => 'PGSQL_FATAL_ERROR', ); private static $diagCodes = array( 'severity' => PGSQL_DIAG_SEVERITY, 'sqlstate' => PGSQL_DIAG_SQLSTATE, 'message' => PGSQL_DIAG_MESSAGE_PRIMARY, 'detail' => PGSQL_DIAG_MESSAGE_DETAIL, 'hint' => PGSQL_DIAG_MESSAGE_HINT, 'statement position' => PGSQL_DIAG_STATEMENT_POSITION, 'internal position' => PGSQL_DIAG_INTERNAL_POSITION, 'internal query' => PGSQL_DIAG_INTERNAL_QUERY, 'context' => PGSQL_DIAG_CONTEXT, 'file' => PGSQL_DIAG_SOURCE_FILE, 'line' => PGSQL_DIAG_SOURCE_LINE, 'function' => PGSQL_DIAG_SOURCE_FUNCTION, ); public static function castLargeObject($lo, array $a, Stub $stub, $isNested) { $a['seek position'] = pg_lo_tell($lo); return $a; } public static function castLink($link, array $a, Stub $stub, $isNested) { $a['status'] = pg_connection_status($link); $a['status'] = new ConstStub(PGSQL_CONNECTION_OK === $a['status'] ? 'PGSQL_CONNECTION_OK' : 'PGSQL_CONNECTION_BAD', $a['status']); $a['busy'] = pg_connection_busy($link); $a['transaction'] = pg_transaction_status($link); if (isset(self::$transactionStatus[$a['transaction']])) { $a['transaction'] = new ConstStub(self::$transactionStatus[$a['transaction']], $a['transaction']); } $a['pid'] = pg_get_pid($link); $a['last error'] = pg_last_error($link); $a['last notice'] = pg_last_notice($link); $a['host'] = pg_host($link); $a['port'] = pg_port($link); $a['dbname'] = pg_dbname($link); $a['options'] = pg_options($link); $a['version'] = pg_version($link); foreach (self::$paramCodes as $v) { if (false !== $s = pg_parameter_status($link, $v)) { $a['param'][$v] = $s; } } $a['param']['client_encoding'] = pg_client_encoding($link); $a['param'] = new EnumStub($a['param']); return $a; } public static function castResult($result, array $a, Stub $stub, $isNested) { $a['num rows'] = pg_num_rows($result); $a['status'] = pg_result_status($result); if (isset(self::$resultStatus[$a['status']])) { $a['status'] = new ConstStub(self::$resultStatus[$a['status']], $a['status']); } $a['command-completion tag'] = pg_result_status($result, PGSQL_STATUS_STRING); if (-1 === $a['num rows']) { foreach (self::$diagCodes as $k => $v) { $a['error'][$k] = pg_result_error_field($result, $v); } } $a['affected rows'] = pg_affected_rows($result); $a['last OID'] = pg_last_oid($result); $fields = pg_num_fields($result); for ($i = 0; $i < $fields; ++$i) { $field = array( 'name' => pg_field_name($result, $i), 'table' => sprintf('%s (OID: %s)', pg_field_table($result, $i), pg_field_table($result, $i, true)), 'type' => sprintf('%s (OID: %s)', pg_field_type($result, $i), pg_field_type_oid($result, $i)), 'nullable' => (bool) pg_field_is_null($result, $i), 'storage' => pg_field_size($result, $i).' bytes', 'display' => pg_field_prtlen($result, $i).' chars', ); if (' (OID: )' === $field['table']) { $field['table'] = null; } if ('-1 bytes' === $field['storage']) { $field['storage'] = 'variable size'; } elseif ('1 bytes' === $field['storage']) { $field['storage'] = '1 byte'; } if ('1 chars' === $field['display']) { $field['display'] = '1 char'; } $a['fields'][] = new EnumStub($field); } return $a; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts Reflector related classes to array representation. * * @author Nicolas Grekas */ class ReflectionCaster { private static $extraMap = array( 'docComment' => 'getDocComment', 'extension' => 'getExtensionName', 'isDisabled' => 'isDisabled', 'isDeprecated' => 'isDeprecated', 'isInternal' => 'isInternal', 'isUserDefined' => 'isUserDefined', 'isGenerator' => 'isGenerator', 'isVariadic' => 'isVariadic', ); /** * @deprecated since Symfony 2.7, to be removed in 3.0. */ public static function castReflector(\Reflector $c, array $a, Stub $stub, $isNested) { @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.7 and will be removed in 3.0.', E_USER_DEPRECATED); $a[Caster::PREFIX_VIRTUAL.'reflection'] = $c->__toString(); return $a; } public static function castClosure(\Closure $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $c = new \ReflectionFunction($c); $stub->class = 'Closure'; // HHVM generates unique class names for closures $a = static::castFunctionAbstract($c, $a, $stub, $isNested); if (isset($a[$prefix.'parameters'])) { foreach ($a[$prefix.'parameters']->value as &$v) { $param = $v; $v = new EnumStub(array()); foreach (static::castParameter($param, array(), $stub, true) as $k => $param) { if ("\0" === $k[0]) { $v->value[substr($k, 3)] = $param; } } unset($v->value['position'], $v->value['isVariadic'], $v->value['byReference'], $v); } } if ($f = $c->getFileName()) { $a[$prefix.'file'] = $f; $a[$prefix.'line'] = $c->getStartLine().' to '.$c->getEndLine(); } $prefix = Caster::PREFIX_DYNAMIC; unset($a['name'], $a[$prefix.'this'], $a[$prefix.'parameter'], $a[Caster::PREFIX_VIRTUAL.'extra']); return $a; } public static function castGenerator(\Generator $c, array $a, Stub $stub, $isNested) { return class_exists('ReflectionGenerator', false) ? self::castReflectionGenerator(new \ReflectionGenerator($c), $a, $stub, $isNested) : $a; } public static function castType(\ReflectionType $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $a += array( $prefix.'name' => $c instanceof \ReflectionNamedType ? $c->getName() : $c->__toString(), $prefix.'allowsNull' => $c->allowsNull(), $prefix.'isBuiltin' => $c->isBuiltin(), ); return $a; } public static function castReflectionGenerator(\ReflectionGenerator $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; if ($c->getThis()) { $a[$prefix.'this'] = new CutStub($c->getThis()); } $x = $c->getFunction(); $frame = array( 'class' => isset($x->class) ? $x->class : null, 'type' => isset($x->class) ? ($x->isStatic() ? '::' : '->') : null, 'function' => $x->name, 'file' => $c->getExecutingFile(), 'line' => $c->getExecutingLine(), ); if ($trace = $c->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS)) { $x = new \ReflectionGenerator($c->getExecutingGenerator()); array_unshift($trace, array( 'function' => 'yield', 'file' => $x->getExecutingFile(), 'line' => $x->getExecutingLine() - 1, )); $trace[] = $frame; $a[$prefix.'trace'] = new TraceStub($trace, false, 0, -1, -1); } else { $x = new FrameStub($frame, false, true); $x = ExceptionCaster::castFrameStub($x, array(), $x, true); $a[$prefix.'executing'] = new EnumStub(array( $frame['class'].$frame['type'].$frame['function'].'()' => $x[$prefix.'src'], )); } return $a; } public static function castClass(\ReflectionClass $c, array $a, Stub $stub, $isNested, $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; if ($n = \Reflection::getModifierNames($c->getModifiers())) { $a[$prefix.'modifiers'] = implode(' ', $n); } self::addMap($a, $c, array( 'extends' => 'getParentClass', 'implements' => 'getInterfaceNames', 'constants' => 'getConstants', )); foreach ($c->getProperties() as $n) { $a[$prefix.'properties'][$n->name] = $n; } foreach ($c->getMethods() as $n) { $a[$prefix.'methods'][$n->name] = $n; } if (!($filter & Caster::EXCLUDE_VERBOSE) && !$isNested) { self::addExtra($a, $c); } return $a; } public static function castFunctionAbstract(\ReflectionFunctionAbstract $c, array $a, Stub $stub, $isNested, $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; self::addMap($a, $c, array( 'returnsReference' => 'returnsReference', 'returnType' => 'getReturnType', 'class' => 'getClosureScopeClass', 'this' => 'getClosureThis', )); if (isset($a[$prefix.'returnType'])) { $v = $a[$prefix.'returnType']; $v = $v instanceof \ReflectionNamedType ? $v->getName() : $v->__toString(); $a[$prefix.'returnType'] = $a[$prefix.'returnType']->allowsNull() ? '?'.$v : $v; } if (isset($a[$prefix.'this'])) { $a[$prefix.'this'] = new CutStub($a[$prefix.'this']); } foreach ($c->getParameters() as $v) { $k = '$'.$v->name; if ($v->isPassedByReference()) { $k = '&'.$k; } if (method_exists($v, 'isVariadic') && $v->isVariadic()) { $k = '...'.$k; } $a[$prefix.'parameters'][$k] = $v; } if (isset($a[$prefix.'parameters'])) { $a[$prefix.'parameters'] = new EnumStub($a[$prefix.'parameters']); } if ($v = $c->getStaticVariables()) { foreach ($v as $k => &$v) { $a[$prefix.'use']['$'.$k] = &$v; } unset($v); $a[$prefix.'use'] = new EnumStub($a[$prefix.'use']); } if (!($filter & Caster::EXCLUDE_VERBOSE) && !$isNested) { self::addExtra($a, $c); } // Added by HHVM unset($a[Caster::PREFIX_DYNAMIC.'static']); return $a; } public static function castMethod(\ReflectionMethod $c, array $a, Stub $stub, $isNested) { $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers())); return $a; } public static function castParameter(\ReflectionParameter $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; // Added by HHVM unset($a['info']); self::addMap($a, $c, array( 'position' => 'getPosition', 'isVariadic' => 'isVariadic', 'byReference' => 'isPassedByReference', 'allowsNull' => 'allowsNull', )); if (method_exists($c, 'getType')) { if ($v = $c->getType()) { $a[$prefix.'typeHint'] = $v instanceof \ReflectionNamedType ? $v->getName() : $v->__toString(); } } elseif (preg_match('/^(?:[^ ]++ ){4}([a-zA-Z_\x7F-\xFF][^ ]++)/', $c, $v)) { $a[$prefix.'typeHint'] = $v[1]; } if (!isset($a[$prefix.'typeHint'])) { unset($a[$prefix.'allowsNull']); } try { $a[$prefix.'default'] = $v = $c->getDefaultValue(); if (method_exists($c, 'isDefaultValueConstant') && $c->isDefaultValueConstant()) { $a[$prefix.'default'] = new ConstStub($c->getDefaultValueConstantName(), $v); } if (null === $v) { unset($a[$prefix.'allowsNull']); } } catch (\ReflectionException $e) { if (isset($a[$prefix.'typeHint']) && $c->allowsNull() && !class_exists('ReflectionNamedType', false)) { $a[$prefix.'default'] = null; unset($a[$prefix.'allowsNull']); } } return $a; } public static function castProperty(\ReflectionProperty $c, array $a, Stub $stub, $isNested) { $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers())); self::addExtra($a, $c); return $a; } public static function castExtension(\ReflectionExtension $c, array $a, Stub $stub, $isNested) { self::addMap($a, $c, array( 'version' => 'getVersion', 'dependencies' => 'getDependencies', 'iniEntries' => 'getIniEntries', 'isPersistent' => 'isPersistent', 'isTemporary' => 'isTemporary', 'constants' => 'getConstants', 'functions' => 'getFunctions', 'classes' => 'getClasses', )); return $a; } public static function castZendExtension(\ReflectionZendExtension $c, array $a, Stub $stub, $isNested) { self::addMap($a, $c, array( 'version' => 'getVersion', 'author' => 'getAuthor', 'copyright' => 'getCopyright', 'url' => 'getURL', )); return $a; } private static function addExtra(&$a, \Reflector $c) { $x = isset($a[Caster::PREFIX_VIRTUAL.'extra']) ? $a[Caster::PREFIX_VIRTUAL.'extra']->value : array(); if (method_exists($c, 'getFileName') && $m = $c->getFileName()) { $x['file'] = $m; $x['line'] = $c->getStartLine().' to '.$c->getEndLine(); } self::addMap($x, $c, self::$extraMap, ''); if ($x) { $a[Caster::PREFIX_VIRTUAL.'extra'] = new EnumStub($x); } } private static function addMap(&$a, \Reflector $c, $map, $prefix = Caster::PREFIX_VIRTUAL) { foreach ($map as $k => $m) { if (method_exists($c, $m) && false !== ($m = $c->$m()) && null !== $m) { $a[$prefix.$k] = $m instanceof \Reflector ? $m->name : $m; } } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts common resource types to array representation. * * @author Nicolas Grekas */ class ResourceCaster { public static function castCurl($h, array $a, Stub $stub, $isNested) { return curl_getinfo($h); } public static function castDba($dba, array $a, Stub $stub, $isNested) { $list = dba_list(); $a['file'] = $list[(int) $dba]; return $a; } public static function castProcess($process, array $a, Stub $stub, $isNested) { return proc_get_status($process); } public static function castStream($stream, array $a, Stub $stub, $isNested) { return stream_get_meta_data($stream) + static::castStreamContext($stream, $a, $stub, $isNested); } public static function castStreamContext($stream, array $a, Stub $stub, $isNested) { return stream_context_get_params($stream); } public static function castGd($gd, array $a, Stub $stub, $isNested) { $a['size'] = imagesx($gd).'x'.imagesy($gd); $a['trueColor'] = imageistruecolor($gd); return $a; } public static function castMysqlLink($h, array $a, Stub $stub, $isNested) { $a['host'] = mysql_get_host_info($h); $a['protocol'] = mysql_get_proto_info($h); $a['server'] = mysql_get_server_info($h); return $a; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts SPL related classes to array representation. * * @author Nicolas Grekas */ class SplCaster { private static $splFileObjectFlags = array( \SplFileObject::DROP_NEW_LINE => 'DROP_NEW_LINE', \SplFileObject::READ_AHEAD => 'READ_AHEAD', \SplFileObject::SKIP_EMPTY => 'SKIP_EMPTY', \SplFileObject::READ_CSV => 'READ_CSV', ); public static function castArrayObject(\ArrayObject $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $class = $stub->class; $flags = $c->getFlags(); $b = array( $prefix.'flag::STD_PROP_LIST' => (bool) ($flags & \ArrayObject::STD_PROP_LIST), $prefix.'flag::ARRAY_AS_PROPS' => (bool) ($flags & \ArrayObject::ARRAY_AS_PROPS), $prefix.'iteratorClass' => $c->getIteratorClass(), $prefix.'storage' => $c->getArrayCopy(), ); if ($class === 'ArrayObject') { $a = $b; } else { if (!($flags & \ArrayObject::STD_PROP_LIST)) { $c->setFlags(\ArrayObject::STD_PROP_LIST); $a = Caster::castObject($c, new \ReflectionClass($class)); $c->setFlags($flags); } $a += $b; } return $a; } public static function castHeap(\Iterator $c, array $a, Stub $stub, $isNested) { $a += array( Caster::PREFIX_VIRTUAL.'heap' => iterator_to_array(clone $c), ); return $a; } public static function castDoublyLinkedList(\SplDoublyLinkedList $c, array $a, Stub $stub, $isNested) { $prefix = Caster::PREFIX_VIRTUAL; $mode = $c->getIteratorMode(); $c->setIteratorMode(\SplDoublyLinkedList::IT_MODE_KEEP | $mode & ~\SplDoublyLinkedList::IT_MODE_DELETE); $a += array( $prefix.'mode' => new ConstStub((($mode & \SplDoublyLinkedList::IT_MODE_LIFO) ? 'IT_MODE_LIFO' : 'IT_MODE_FIFO').' | '.(($mode & \SplDoublyLinkedList::IT_MODE_DELETE) ? 'IT_MODE_DELETE' : 'IT_MODE_KEEP'), $mode), $prefix.'dllist' => iterator_to_array($c), ); $c->setIteratorMode($mode); return $a; } public static function castFileInfo(\SplFileInfo $c, array $a, Stub $stub, $isNested) { static $map = array( 'path' => 'getPath', 'filename' => 'getFilename', 'basename' => 'getBasename', 'pathname' => 'getPathname', 'extension' => 'getExtension', 'realPath' => 'getRealPath', 'aTime' => 'getATime', 'mTime' => 'getMTime', 'cTime' => 'getCTime', 'inode' => 'getInode', 'size' => 'getSize', 'perms' => 'getPerms', 'owner' => 'getOwner', 'group' => 'getGroup', 'type' => 'getType', 'writable' => 'isWritable', 'readable' => 'isReadable', 'executable' => 'isExecutable', 'file' => 'isFile', 'dir' => 'isDir', 'link' => 'isLink', 'linkTarget' => 'getLinkTarget', ); $prefix = Caster::PREFIX_VIRTUAL; foreach ($map as $key => $accessor) { try { $a[$prefix.$key] = $c->$accessor(); } catch (\Exception $e) { } } if (isset($a[$prefix.'perms'])) { $a[$prefix.'perms'] = new ConstStub(sprintf('0%o', $a[$prefix.'perms']), $a[$prefix.'perms']); } static $mapDate = array('aTime', 'mTime', 'cTime'); foreach ($mapDate as $key) { if (isset($a[$prefix.$key])) { $a[$prefix.$key] = new ConstStub(date('Y-m-d H:i:s', $a[$prefix.$key]), $a[$prefix.$key]); } } return $a; } public static function castFileObject(\SplFileObject $c, array $a, Stub $stub, $isNested) { static $map = array( 'csvControl' => 'getCsvControl', 'flags' => 'getFlags', 'maxLineLen' => 'getMaxLineLen', 'fstat' => 'fstat', 'eof' => 'eof', 'key' => 'key', ); $prefix = Caster::PREFIX_VIRTUAL; foreach ($map as $key => $accessor) { try { $a[$prefix.$key] = $c->$accessor(); } catch (\Exception $e) { } } if (isset($a[$prefix.'flags'])) { $flagsArray = array(); foreach (self::$splFileObjectFlags as $value => $name) { if ($a[$prefix.'flags'] & $value) { $flagsArray[] = $name; } } $a[$prefix.'flags'] = new ConstStub(implode('|', $flagsArray), $a[$prefix.'flags']); } if (isset($a[$prefix.'fstat'])) { $a[$prefix.'fstat'] = new CutArrayStub($a[$prefix.'fstat'], array('dev', 'ino', 'nlink', 'rdev', 'blksize', 'blocks')); } return $a; } public static function castFixedArray(\SplFixedArray $c, array $a, Stub $stub, $isNested) { $a += array( Caster::PREFIX_VIRTUAL.'storage' => $c->toArray(), ); return $a; } public static function castObjectStorage(\SplObjectStorage $c, array $a, Stub $stub, $isNested) { $storage = array(); unset($a[Caster::PREFIX_DYNAMIC."\0gcdata"]); // Don't hit https://bugs.php.net/65967 foreach ($c as $obj) { $storage[spl_object_hash($obj)] = array( 'object' => $obj, 'info' => $c->getInfo(), ); } $a += array( Caster::PREFIX_VIRTUAL.'storage' => $storage, ); return $a; } public static function castOuterIterator(\OuterIterator $c, array $a, Stub $stub, $isNested) { $a[Caster::PREFIX_VIRTUAL.'innerIterator'] = $c->getInnerIterator(); return $a; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts a caster's Stub. * * @author Nicolas Grekas */ class StubCaster { public static function castStub(Stub $c, array $a, Stub $stub, $isNested) { if ($isNested) { $stub->type = $c->type; $stub->class = $c->class; $stub->value = $c->value; $stub->handle = $c->handle; $stub->cut = $c->cut; return array(); } } public static function castCutArray(CutArrayStub $c, array $a, Stub $stub, $isNested) { return $isNested ? $c->preservedSubset : $a; } public static function cutInternals($obj, array $a, Stub $stub, $isNested) { if ($isNested) { $stub->cut += count($a); return array(); } return $a; } public static function castEnum(EnumStub $c, array $a, Stub $stub, $isNested) { if ($isNested) { $stub->class = ''; $stub->handle = 0; $stub->value = null; $a = array(); if ($c->value) { foreach (array_keys($c->value) as $k) { $keys[] = !isset($k[0]) || "\0" !== $k[0] ? Caster::PREFIX_VIRTUAL.$k : $k; } // Preserve references with array_combine() $a = array_combine($keys, $c->value); } } return $a; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Represents a backtrace as returned by debug_backtrace() or Exception->getTrace(). * * @author Nicolas Grekas */ class TraceStub extends Stub { public $keepArgs; public $sliceOffset; public $sliceLength; public $numberingOffset; public function __construct(array $trace, $keepArgs = true, $sliceOffset = 0, $sliceLength = null, $numberingOffset = 0) { $this->value = $trace; $this->keepArgs = $keepArgs; $this->sliceOffset = $sliceOffset; $this->sliceLength = $sliceLength; $this->numberingOffset = $numberingOffset; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Caster; use Symfony\Component\VarDumper\Cloner\Stub; /** * Casts XML resources to array representation. * * @author Nicolas Grekas */ class XmlResourceCaster { private static $xmlErrors = array( XML_ERROR_NONE => 'XML_ERROR_NONE', XML_ERROR_NO_MEMORY => 'XML_ERROR_NO_MEMORY', XML_ERROR_SYNTAX => 'XML_ERROR_SYNTAX', XML_ERROR_NO_ELEMENTS => 'XML_ERROR_NO_ELEMENTS', XML_ERROR_INVALID_TOKEN => 'XML_ERROR_INVALID_TOKEN', XML_ERROR_UNCLOSED_TOKEN => 'XML_ERROR_UNCLOSED_TOKEN', XML_ERROR_PARTIAL_CHAR => 'XML_ERROR_PARTIAL_CHAR', XML_ERROR_TAG_MISMATCH => 'XML_ERROR_TAG_MISMATCH', XML_ERROR_DUPLICATE_ATTRIBUTE => 'XML_ERROR_DUPLICATE_ATTRIBUTE', XML_ERROR_JUNK_AFTER_DOC_ELEMENT => 'XML_ERROR_JUNK_AFTER_DOC_ELEMENT', XML_ERROR_PARAM_ENTITY_REF => 'XML_ERROR_PARAM_ENTITY_REF', XML_ERROR_UNDEFINED_ENTITY => 'XML_ERROR_UNDEFINED_ENTITY', XML_ERROR_RECURSIVE_ENTITY_REF => 'XML_ERROR_RECURSIVE_ENTITY_REF', XML_ERROR_ASYNC_ENTITY => 'XML_ERROR_ASYNC_ENTITY', XML_ERROR_BAD_CHAR_REF => 'XML_ERROR_BAD_CHAR_REF', XML_ERROR_BINARY_ENTITY_REF => 'XML_ERROR_BINARY_ENTITY_REF', XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF => 'XML_ERROR_ATTRIBUTE_EXTERNAL_ENTITY_REF', XML_ERROR_MISPLACED_XML_PI => 'XML_ERROR_MISPLACED_XML_PI', XML_ERROR_UNKNOWN_ENCODING => 'XML_ERROR_UNKNOWN_ENCODING', XML_ERROR_INCORRECT_ENCODING => 'XML_ERROR_INCORRECT_ENCODING', XML_ERROR_UNCLOSED_CDATA_SECTION => 'XML_ERROR_UNCLOSED_CDATA_SECTION', XML_ERROR_EXTERNAL_ENTITY_HANDLING => 'XML_ERROR_EXTERNAL_ENTITY_HANDLING', ); public static function castXml($h, array $a, Stub $stub, $isNested) { $a['current_byte_index'] = xml_get_current_byte_index($h); $a['current_column_number'] = xml_get_current_column_number($h); $a['current_line_number'] = xml_get_current_line_number($h); $a['error_code'] = xml_get_error_code($h); if (isset(self::$xmlErrors[$a['error_code']])) { $a['error_code'] = new ConstStub(self::$xmlErrors[$a['error_code']], $a['error_code']); } return $a; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Exception\ThrowingCasterException; /** * AbstractCloner implements a generic caster mechanism for objects and resources. * * @author Nicolas Grekas */ abstract class AbstractCloner implements ClonerInterface { public static $defaultCasters = array( 'Symfony\Component\VarDumper\Caster\CutStub' => 'Symfony\Component\VarDumper\Caster\StubCaster::castStub', 'Symfony\Component\VarDumper\Caster\CutArrayStub' => 'Symfony\Component\VarDumper\Caster\StubCaster::castCutArray', 'Symfony\Component\VarDumper\Caster\ConstStub' => 'Symfony\Component\VarDumper\Caster\StubCaster::castStub', 'Symfony\Component\VarDumper\Caster\EnumStub' => 'Symfony\Component\VarDumper\Caster\StubCaster::castEnum', 'Closure' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castClosure', 'Generator' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castGenerator', 'ReflectionType' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castType', 'ReflectionGenerator' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castReflectionGenerator', 'ReflectionClass' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castClass', 'ReflectionFunctionAbstract' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castFunctionAbstract', 'ReflectionMethod' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castMethod', 'ReflectionParameter' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castParameter', 'ReflectionProperty' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castProperty', 'ReflectionExtension' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castExtension', 'ReflectionZendExtension' => 'Symfony\Component\VarDumper\Caster\ReflectionCaster::castZendExtension', 'Doctrine\Common\Persistence\ObjectManager' => 'Symfony\Component\VarDumper\Caster\StubCaster::cutInternals', 'Doctrine\Common\Proxy\Proxy' => 'Symfony\Component\VarDumper\Caster\DoctrineCaster::castCommonProxy', 'Doctrine\ORM\Proxy\Proxy' => 'Symfony\Component\VarDumper\Caster\DoctrineCaster::castOrmProxy', 'Doctrine\ORM\PersistentCollection' => 'Symfony\Component\VarDumper\Caster\DoctrineCaster::castPersistentCollection', 'DOMException' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castException', 'DOMStringList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength', 'DOMNameList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength', 'DOMImplementation' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castImplementation', 'DOMImplementationList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength', 'DOMNode' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castNode', 'DOMNameSpaceNode' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castNameSpaceNode', 'DOMDocument' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castDocument', 'DOMNodeList' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength', 'DOMNamedNodeMap' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLength', 'DOMCharacterData' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castCharacterData', 'DOMAttr' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castAttr', 'DOMElement' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castElement', 'DOMText' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castText', 'DOMTypeinfo' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castTypeinfo', 'DOMDomError' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castDomError', 'DOMLocator' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castLocator', 'DOMDocumentType' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castDocumentType', 'DOMNotation' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castNotation', 'DOMEntity' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castEntity', 'DOMProcessingInstruction' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castProcessingInstruction', 'DOMXPath' => 'Symfony\Component\VarDumper\Caster\DOMCaster::castXPath', 'ErrorException' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castErrorException', 'Exception' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castException', 'Error' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castError', 'Symfony\Component\DependencyInjection\ContainerInterface' => 'Symfony\Component\VarDumper\Caster\StubCaster::cutInternals', 'Symfony\Component\VarDumper\Exception\ThrowingCasterException' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castThrowingCasterException', 'Symfony\Component\VarDumper\Caster\TraceStub' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castTraceStub', 'Symfony\Component\VarDumper\Caster\FrameStub' => 'Symfony\Component\VarDumper\Caster\ExceptionCaster::castFrameStub', 'PHPUnit_Framework_MockObject_MockObject' => 'Symfony\Component\VarDumper\Caster\StubCaster::cutInternals', 'Prophecy\Prophecy\ProphecySubjectInterface' => 'Symfony\Component\VarDumper\Caster\StubCaster::cutInternals', 'Mockery\MockInterface' => 'Symfony\Component\VarDumper\Caster\StubCaster::cutInternals', 'PDO' => 'Symfony\Component\VarDumper\Caster\PdoCaster::castPdo', 'PDOStatement' => 'Symfony\Component\VarDumper\Caster\PdoCaster::castPdoStatement', 'AMQPConnection' => 'Symfony\Component\VarDumper\Caster\AmqpCaster::castConnection', 'AMQPChannel' => 'Symfony\Component\VarDumper\Caster\AmqpCaster::castChannel', 'AMQPQueue' => 'Symfony\Component\VarDumper\Caster\AmqpCaster::castQueue', 'AMQPExchange' => 'Symfony\Component\VarDumper\Caster\AmqpCaster::castExchange', 'AMQPEnvelope' => 'Symfony\Component\VarDumper\Caster\AmqpCaster::castEnvelope', 'ArrayObject' => 'Symfony\Component\VarDumper\Caster\SplCaster::castArrayObject', 'SplDoublyLinkedList' => 'Symfony\Component\VarDumper\Caster\SplCaster::castDoublyLinkedList', 'SplFileInfo' => 'Symfony\Component\VarDumper\Caster\SplCaster::castFileInfo', 'SplFileObject' => 'Symfony\Component\VarDumper\Caster\SplCaster::castFileObject', 'SplFixedArray' => 'Symfony\Component\VarDumper\Caster\SplCaster::castFixedArray', 'SplHeap' => 'Symfony\Component\VarDumper\Caster\SplCaster::castHeap', 'SplObjectStorage' => 'Symfony\Component\VarDumper\Caster\SplCaster::castObjectStorage', 'SplPriorityQueue' => 'Symfony\Component\VarDumper\Caster\SplCaster::castHeap', 'OuterIterator' => 'Symfony\Component\VarDumper\Caster\SplCaster::castOuterIterator', 'MongoCursorInterface' => 'Symfony\Component\VarDumper\Caster\MongoCaster::castCursor', ':curl' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castCurl', ':dba' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castDba', ':dba persistent' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castDba', ':gd' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castGd', ':mysql link' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castMysqlLink', ':pgsql large object' => 'Symfony\Component\VarDumper\Caster\PgSqlCaster::castLargeObject', ':pgsql link' => 'Symfony\Component\VarDumper\Caster\PgSqlCaster::castLink', ':pgsql link persistent' => 'Symfony\Component\VarDumper\Caster\PgSqlCaster::castLink', ':pgsql result' => 'Symfony\Component\VarDumper\Caster\PgSqlCaster::castResult', ':process' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castProcess', ':stream' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castStream', ':stream-context' => 'Symfony\Component\VarDumper\Caster\ResourceCaster::castStreamContext', ':xml' => 'Symfony\Component\VarDumper\Caster\XmlResourceCaster::castXml', ); protected $maxItems = 2500; protected $maxString = -1; protected $useExt; private $casters = array(); private $prevErrorHandler; private $classInfo = array(); private $filter = 0; /** * @param callable[]|null $casters A map of casters * * @see addCasters */ public function __construct(array $casters = null) { if (null === $casters) { $casters = static::$defaultCasters; } $this->addCasters($casters); $this->useExt = extension_loaded('symfony_debug'); } /** * Adds casters for resources and objects. * * Maps resources or objects types to a callback. * Types are in the key, with a callable caster for value. * Resource types are to be prefixed with a `:`, * see e.g. static::$defaultCasters. * * @param callable[] $casters A map of casters */ public function addCasters(array $casters) { foreach ($casters as $type => $callback) { $this->casters[strtolower($type)][] = $callback; } } /** * Sets the maximum number of items to clone past the first level in nested structures. * * @param int $maxItems */ public function setMaxItems($maxItems) { $this->maxItems = (int) $maxItems; } /** * Sets the maximum cloned length for strings. * * @param int $maxString */ public function setMaxString($maxString) { $this->maxString = (int) $maxString; } /** * Clones a PHP variable. * * @param mixed $var Any PHP variable * @param int $filter A bit field of Caster::EXCLUDE_* constants * * @return Data The cloned variable represented by a Data object */ public function cloneVar($var, $filter = 0) { $this->filter = $filter; $this->prevErrorHandler = set_error_handler(array($this, 'handleError')); try { $data = $this->doClone($var); } catch (\Exception $e) { } restore_error_handler(); $this->prevErrorHandler = null; if (isset($e)) { throw $e; } return new Data($data); } /** * Effectively clones the PHP variable. * * @param mixed $var Any PHP variable * * @return array The cloned variable represented in an array */ abstract protected function doClone($var); /** * Casts an object to an array representation. * * @param Stub $stub The Stub for the casted object * @param bool $isNested True if the object is nested in the dumped structure * * @return array The object casted as array */ protected function castObject(Stub $stub, $isNested) { $obj = $stub->value; $class = $stub->class; if (isset($class[15]) && "\0" === $class[15] && 0 === strpos($class, "class@anonymous\x00")) { $stub->class = get_parent_class($class).'@anonymous'; } if (isset($this->classInfo[$class])) { $classInfo = $this->classInfo[$class]; } else { $classInfo = array( new \ReflectionClass($class), array_reverse(array($class => $class) + class_parents($class) + class_implements($class) + array('*' => '*')), ); $this->classInfo[$class] = $classInfo; } $a = $this->callCaster('Symfony\Component\VarDumper\Caster\Caster::castObject', $obj, $classInfo[0], null, $isNested); foreach ($classInfo[1] as $p) { if (!empty($this->casters[$p = strtolower($p)])) { foreach ($this->casters[$p] as $p) { $a = $this->callCaster($p, $obj, $a, $stub, $isNested); } } } return $a; } /** * Casts a resource to an array representation. * * @param Stub $stub The Stub for the casted resource * @param bool $isNested True if the object is nested in the dumped structure * * @return array The resource casted as array */ protected function castResource(Stub $stub, $isNested) { $a = array(); $res = $stub->value; $type = $stub->class; if (!empty($this->casters[':'.$type])) { foreach ($this->casters[':'.$type] as $c) { $a = $this->callCaster($c, $res, $a, $stub, $isNested); } } return $a; } /** * Calls a custom caster. * * @param callable $callback The caster * @param object|resource $obj The object/resource being casted * @param array $a The result of the previous cast for chained casters * @param Stub $stub The Stub for the casted object/resource * @param bool $isNested True if $obj is nested in the dumped structure * * @return array The casted object/resource */ private function callCaster($callback, $obj, $a, $stub, $isNested) { try { $cast = call_user_func($callback, $obj, $a, $stub, $isNested, $this->filter); if (is_array($cast)) { $a = $cast; } } catch (\Exception $e) { $a[(Stub::TYPE_OBJECT === $stub->type ? Caster::PREFIX_VIRTUAL : '').'⚠'] = new ThrowingCasterException($e); } return $a; } /** * Special handling for errors: cloning must be fail-safe. * * @internal */ public function handleError($type, $msg, $file, $line, $context) { if (E_RECOVERABLE_ERROR === $type || E_USER_ERROR === $type) { // Cloner never dies throw new \ErrorException($msg, 0, $type, $file, $line); } if ($this->prevErrorHandler) { return call_user_func($this->prevErrorHandler, $type, $msg, $file, $line, $context); } return false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * @author Nicolas Grekas */ interface ClonerInterface { /** * Clones a PHP variable. * * @param mixed $var Any PHP variable * * @return Data The cloned variable represented by a Data object */ public function cloneVar($var); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * Represents the current state of a dumper while dumping. * * @author Nicolas Grekas */ class Cursor { const HASH_INDEXED = Stub::ARRAY_INDEXED; const HASH_ASSOC = Stub::ARRAY_ASSOC; const HASH_OBJECT = Stub::TYPE_OBJECT; const HASH_RESOURCE = Stub::TYPE_RESOURCE; public $depth = 0; public $refIndex = 0; public $softRefTo = 0; public $softRefCount = 0; public $softRefHandle = 0; public $hardRefTo = 0; public $hardRefCount = 0; public $hardRefHandle = 0; public $hashType; public $hashKey; public $hashKeyIsBinary; public $hashIndex = 0; public $hashLength = 0; public $hashCut = 0; public $stop = false; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * @author Nicolas Grekas */ class Data { private $data; private $maxDepth = 20; private $maxItemsPerDepth = -1; private $useRefHandles = -1; /** * @param array $data A array as returned by ClonerInterface::cloneVar() */ public function __construct(array $data) { $this->data = $data; } /** * @return array The raw data structure */ public function getRawData() { return $this->data; } /** * Returns a depth limited clone of $this. * * @param int $maxDepth The max dumped depth level * * @return self A clone of $this */ public function withMaxDepth($maxDepth) { $data = clone $this; $data->maxDepth = (int) $maxDepth; return $data; } /** * Limits the number of elements per depth level. * * @param int $maxItemsPerDepth The max number of items dumped per depth level * * @return self A clone of $this */ public function withMaxItemsPerDepth($maxItemsPerDepth) { $data = clone $this; $data->maxItemsPerDepth = (int) $maxItemsPerDepth; return $data; } /** * Enables/disables objects' identifiers tracking. * * @param bool $useRefHandles False to hide global ref. handles * * @return self A clone of $this */ public function withRefHandles($useRefHandles) { $data = clone $this; $data->useRefHandles = $useRefHandles ? -1 : 0; return $data; } /** * Returns a depth limited clone of $this. * * @param int $maxDepth The max dumped depth level * @param int $maxItemsPerDepth The max number of items dumped per depth level * @param bool $useRefHandles False to hide ref. handles * * @return self A depth limited clone of $this * * @deprecated since Symfony 2.7, to be removed in 3.0. Use withMaxDepth, withMaxItemsPerDepth or withRefHandles instead. */ public function getLimitedClone($maxDepth, $maxItemsPerDepth, $useRefHandles = true) { @trigger_error('The '.__METHOD__.' method is deprecated since Symfony 2.7 and will be removed in 3.0. Use withMaxDepth, withMaxItemsPerDepth or withRefHandles methods instead.', E_USER_DEPRECATED); $data = clone $this; $data->maxDepth = (int) $maxDepth; $data->maxItemsPerDepth = (int) $maxItemsPerDepth; $data->useRefHandles = $useRefHandles ? -1 : 0; return $data; } /** * Dumps data with a DumperInterface dumper. */ public function dump(DumperInterface $dumper) { $refs = array(0); $this->dumpItem($dumper, new Cursor(), $refs, $this->data[0][0]); } /** * Depth-first dumping of items. * * @param DumperInterface $dumper The dumper being used for dumping * @param Cursor $cursor A cursor used for tracking dumper state position * @param array &$refs A map of all references discovered while dumping * @param mixed $item A Stub object or the original value being dumped */ private function dumpItem($dumper, $cursor, &$refs, $item) { $cursor->refIndex = 0; $cursor->softRefTo = $cursor->softRefHandle = $cursor->softRefCount = 0; $cursor->hardRefTo = $cursor->hardRefHandle = $cursor->hardRefCount = 0; $firstSeen = true; if (!$item instanceof Stub) { $type = gettype($item); } elseif (Stub::TYPE_REF === $item->type) { if ($item->handle) { if (!isset($refs[$r = $item->handle - (PHP_INT_MAX >> 1)])) { $cursor->refIndex = $refs[$r] = $cursor->refIndex ?: ++$refs[0]; } else { $firstSeen = false; } $cursor->hardRefTo = $refs[$r]; $cursor->hardRefHandle = $this->useRefHandles & $item->handle; $cursor->hardRefCount = $item->refCount; } $type = $item->class ?: gettype($item->value); $item = $item->value; } if ($item instanceof Stub) { if ($item->refCount) { if (!isset($refs[$r = $item->handle])) { $cursor->refIndex = $refs[$r] = $cursor->refIndex ?: ++$refs[0]; } else { $firstSeen = false; } $cursor->softRefTo = $refs[$r]; } $cursor->softRefHandle = $this->useRefHandles & $item->handle; $cursor->softRefCount = $item->refCount; $cut = $item->cut; if ($item->position && $firstSeen) { $children = $this->data[$item->position]; if ($cursor->stop) { if ($cut >= 0) { $cut += count($children); } $children = array(); } } else { $children = array(); } switch ($item->type) { case Stub::TYPE_STRING: $dumper->dumpString($cursor, $item->value, Stub::STRING_BINARY === $item->class, $cut); break; case Stub::TYPE_ARRAY: $item = clone $item; $item->type = $item->class; $item->class = $item->value; // No break; case Stub::TYPE_OBJECT: case Stub::TYPE_RESOURCE: $withChildren = $children && $cursor->depth !== $this->maxDepth && $this->maxItemsPerDepth; $dumper->enterHash($cursor, $item->type, $item->class, $withChildren); if ($withChildren) { $cut = $this->dumpChildren($dumper, $cursor, $refs, $children, $cut, $item->type); } elseif ($children && 0 <= $cut) { $cut += count($children); } $dumper->leaveHash($cursor, $item->type, $item->class, $withChildren, $cut); break; default: throw new \RuntimeException(sprintf('Unexpected Stub type: %s', $item->type)); } } elseif ('array' === $type) { $dumper->enterHash($cursor, Cursor::HASH_INDEXED, 0, false); $dumper->leaveHash($cursor, Cursor::HASH_INDEXED, 0, false, 0); } elseif ('string' === $type) { $dumper->dumpString($cursor, $item, false, 0); } else { $dumper->dumpScalar($cursor, $type, $item); } } /** * Dumps children of hash structures. * * @param DumperInterface $dumper * @param Cursor $parentCursor The cursor of the parent hash * @param array &$refs A map of all references discovered while dumping * @param array $children The children to dump * @param int $hashCut The number of items removed from the original hash * @param string $hashType A Cursor::HASH_* const * * @return int The final number of removed items */ private function dumpChildren($dumper, $parentCursor, &$refs, $children, $hashCut, $hashType) { $cursor = clone $parentCursor; ++$cursor->depth; $cursor->hashType = $hashType; $cursor->hashIndex = 0; $cursor->hashLength = count($children); $cursor->hashCut = $hashCut; foreach ($children as $key => $child) { $cursor->hashKeyIsBinary = isset($key[0]) && !preg_match('//u', $key); $cursor->hashKey = $key; $this->dumpItem($dumper, $cursor, $refs, $child); if (++$cursor->hashIndex === $this->maxItemsPerDepth || $cursor->stop) { $parentCursor->stop = true; return $hashCut >= 0 ? $hashCut + $cursor->hashLength - $cursor->hashIndex : $hashCut; } } return $hashCut; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * DumperInterface used by Data objects. * * @author Nicolas Grekas */ interface DumperInterface { /** * Dumps a scalar value. * * @param Cursor $cursor The Cursor position in the dump * @param string $type The PHP type of the value being dumped * @param scalar $value The scalar value being dumped */ public function dumpScalar(Cursor $cursor, $type, $value); /** * Dumps a string. * * @param Cursor $cursor The Cursor position in the dump * @param string $str The string being dumped * @param bool $bin Whether $str is UTF-8 or binary encoded * @param int $cut The number of characters $str has been cut by */ public function dumpString(Cursor $cursor, $str, $bin, $cut); /** * Dumps while entering an hash. * * @param Cursor $cursor The Cursor position in the dump * @param int $type A Cursor::HASH_* const for the type of hash * @param string $class The object class, resource type or array count * @param bool $hasChild When the dump of the hash has child item */ public function enterHash(Cursor $cursor, $type, $class, $hasChild); /** * Dumps while leaving an hash. * * @param Cursor $cursor The Cursor position in the dump * @param int $type A Cursor::HASH_* const for the type of hash * @param string $class The object class, resource type or array count * @param bool $hasChild When the dump of the hash has child item * @param int $cut The number of items the hash has been cut by */ public function leaveHash(Cursor $cursor, $type, $class, $hasChild, $cut); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * Represents the main properties of a PHP variable. * * @author Nicolas Grekas */ class Stub { const TYPE_REF = 'ref'; const TYPE_STRING = 'string'; const TYPE_ARRAY = 'array'; const TYPE_OBJECT = 'object'; const TYPE_RESOURCE = 'resource'; const STRING_BINARY = 'bin'; const STRING_UTF8 = 'utf8'; const ARRAY_ASSOC = 'assoc'; const ARRAY_INDEXED = 'indexed'; public $type = self::TYPE_REF; public $class = ''; public $value; public $cut = 0; public $handle = 0; public $refCount = 0; public $position = 0; } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Cloner; /** * @author Nicolas Grekas */ class VarCloner extends AbstractCloner { private static $hashMask = 0; private static $hashOffset = 0; /** * {@inheritdoc} */ protected function doClone($var) { $useExt = $this->useExt; $len = 1; // Length of $queue $pos = 0; // Number of cloned items past the first level $refsCounter = 0; // Hard references counter $queue = array(array($var)); // This breadth-first queue is the return value $arrayRefs = array(); // Map of queue indexes to stub array objects $hardRefs = array(); // Map of original zval hashes to stub objects $objRefs = array(); // Map of original object handles to their stub object couterpart $resRefs = array(); // Map of original resource handles to their stub object couterpart $values = array(); // Map of stub objects' hashes to original values $maxItems = $this->maxItems; $maxString = $this->maxString; $cookie = (object) array(); // Unique object used to detect hard references $gid = uniqid(mt_rand(), true); // Unique string used to detect the special $GLOBALS variable $a = null; // Array cast for nested structures $stub = null; // Stub capturing the main properties of an original item value // or null if the original value is used directly $zval = array( // Main properties of the current value 'type' => null, 'zval_isref' => null, 'zval_hash' => null, 'array_count' => null, 'object_class' => null, 'object_handle' => null, 'resource_type' => null, ); if (!self::$hashMask) { self::initHashMask(); } $hashMask = self::$hashMask; $hashOffset = self::$hashOffset; for ($i = 0; $i < $len; ++$i) { $indexed = true; // Whether the currently iterated array is numerically indexed or not $j = -1; // Position in the currently iterated array $fromObjCast = array_keys($queue[$i]); $fromObjCast = array_keys(array_flip($fromObjCast)) !== $fromObjCast; $refs = $vals = $fromObjCast ? array_values($queue[$i]) : $queue[$i]; foreach ($queue[$i] as $k => $v) { // $k is the original key // $v is the original value or a stub object in case of hard references if ($k !== ++$j) { $indexed = false; } if ($fromObjCast) { $k = $j; } if ($useExt) { $zval = symfony_zval_info($k, $refs); } else { $refs[$k] = $cookie; if ($zval['zval_isref'] = $vals[$k] === $cookie) { $zval['zval_hash'] = $v instanceof Stub ? spl_object_hash($v) : null; } $zval['type'] = gettype($v); } if ($zval['zval_isref']) { $vals[$k] = &$stub; // Break hard references to make $queue completely unset($stub); // independent from the original structure if (isset($hardRefs[$zval['zval_hash']])) { $vals[$k] = $useExt ? ($v = $hardRefs[$zval['zval_hash']]) : ($refs[$k] = $v); if ($v->value instanceof Stub && (Stub::TYPE_OBJECT === $v->value->type || Stub::TYPE_RESOURCE === $v->value->type)) { ++$v->value->refCount; } ++$v->refCount; continue; } } // Create $stub when the original value $v can not be used directly // If $v is a nested structure, put that structure in array $a switch ($zval['type']) { case 'string': if (isset($v[0]) && !preg_match('//u', $v)) { $stub = new Stub(); $stub->type = Stub::TYPE_STRING; $stub->class = Stub::STRING_BINARY; if (0 <= $maxString && 0 < $cut = strlen($v) - $maxString) { $stub->cut = $cut; $stub->value = substr($v, 0, -$cut); } else { $stub->value = $v; } } elseif (0 <= $maxString && isset($v[1 + ($maxString >> 2)]) && 0 < $cut = mb_strlen($v, 'UTF-8') - $maxString) { $stub = new Stub(); $stub->type = Stub::TYPE_STRING; $stub->class = Stub::STRING_UTF8; $stub->cut = $cut; $stub->value = mb_substr($v, 0, $maxString, 'UTF-8'); } break; case 'integer': break; case 'array': if ($v) { $stub = $arrayRefs[$len] = new Stub(); $stub->type = Stub::TYPE_ARRAY; $stub->class = Stub::ARRAY_ASSOC; // Copies of $GLOBALS have very strange behavior, // let's detect them with some black magic $a = $v; $a[$gid] = true; // Happens with copies of $GLOBALS if (isset($v[$gid])) { unset($v[$gid]); $a = array(); foreach ($v as $gk => &$gv) { $a[$gk] = &$gv; } } else { $a = $v; } $stub->value = $zval['array_count'] ?: count($a); } break; case 'object': if (empty($objRefs[$h = $zval['object_handle'] ?: ($hashMask ^ hexdec(substr(spl_object_hash($v), $hashOffset, PHP_INT_SIZE)))])) { $stub = new Stub(); $stub->type = Stub::TYPE_OBJECT; $stub->class = $zval['object_class'] ?: get_class($v); $stub->value = $v; $stub->handle = $h; $a = $this->castObject($stub, 0 < $i); if ($v !== $stub->value) { if (Stub::TYPE_OBJECT !== $stub->type || null === $stub->value) { break; } if ($useExt) { $zval['type'] = $stub->value; $zval = symfony_zval_info('type', $zval); $h = $zval['object_handle']; } else { $h = $hashMask ^ hexdec(substr(spl_object_hash($stub->value), $hashOffset, PHP_INT_SIZE)); } $stub->handle = $h; } $stub->value = null; if (0 <= $maxItems && $maxItems <= $pos) { $stub->cut = count($a); $a = null; } } if (empty($objRefs[$h])) { $objRefs[$h] = $stub; } else { $stub = $objRefs[$h]; ++$stub->refCount; $a = null; } break; case 'resource': case 'unknown type': case 'resource (closed)': if (empty($resRefs[$h = (int) $v])) { $stub = new Stub(); $stub->type = Stub::TYPE_RESOURCE; if ('Unknown' === $stub->class = $zval['resource_type'] ?: @get_resource_type($v)) { $stub->class = 'Closed'; } $stub->value = $v; $stub->handle = $h; $a = $this->castResource($stub, 0 < $i); $stub->value = null; if (0 <= $maxItems && $maxItems <= $pos) { $stub->cut = count($a); $a = null; } } if (empty($resRefs[$h])) { $resRefs[$h] = $stub; } else { $stub = $resRefs[$h]; ++$stub->refCount; $a = null; } break; } if (isset($stub)) { if ($zval['zval_isref']) { if ($useExt) { $vals[$k] = $hardRefs[$zval['zval_hash']] = $v = new Stub(); $v->value = $stub; } else { $refs[$k] = new Stub(); $refs[$k]->value = $stub; $h = spl_object_hash($refs[$k]); $vals[$k] = $hardRefs[$h] = &$refs[$k]; $values[$h] = $v; } $vals[$k]->handle = ++$refsCounter; } else { $vals[$k] = $stub; } if ($a) { if ($i && 0 <= $maxItems) { $k = count($a); if ($pos < $maxItems) { if ($maxItems < $pos += $k) { $a = array_slice($a, 0, $maxItems - $pos); if ($stub->cut >= 0) { $stub->cut += $pos - $maxItems; } } } else { if ($stub->cut >= 0) { $stub->cut += $k; } $stub = $a = null; unset($arrayRefs[$len]); continue; } } $queue[$len] = $a; $stub->position = $len++; } $stub = $a = null; } elseif ($zval['zval_isref']) { if ($useExt) { $vals[$k] = $hardRefs[$zval['zval_hash']] = new Stub(); $vals[$k]->value = $v; } else { $refs[$k] = $vals[$k] = new Stub(); $refs[$k]->value = $v; $h = spl_object_hash($refs[$k]); $hardRefs[$h] = &$refs[$k]; $values[$h] = $v; } $vals[$k]->handle = ++$refsCounter; } } if ($fromObjCast) { $refs = $vals; $vals = array(); $j = -1; foreach ($queue[$i] as $k => $v) { foreach (array($k => $v) as $a => $v) { } if ($a !== $k) { $vals = (object) $vals; $vals->{$k} = $refs[++$j]; $vals = (array) $vals; } else { $vals[$k] = $refs[++$j]; } } } $queue[$i] = $vals; if (isset($arrayRefs[$i])) { if ($indexed) { $arrayRefs[$i]->class = Stub::ARRAY_INDEXED; } unset($arrayRefs[$i]); } } foreach ($values as $h => $v) { $hardRefs[$h] = $v; } return $queue; } private static function initHashMask() { $obj = (object) array(); self::$hashOffset = 16 - PHP_INT_SIZE; self::$hashMask = -1; if (defined('HHVM_VERSION')) { self::$hashOffset += 16; } else { // check if we are nested in an output buffering handler to prevent a fatal error with ob_start() below $obFuncs = array('ob_clean', 'ob_end_clean', 'ob_flush', 'ob_end_flush', 'ob_get_contents', 'ob_get_flush'); foreach (debug_backtrace(PHP_VERSION_ID >= 50400 ? DEBUG_BACKTRACE_IGNORE_ARGS : false) as $frame) { if (isset($frame['function'][0]) && !isset($frame['class']) && 'o' === $frame['function'][0] && in_array($frame['function'], $obFuncs)) { $frame['line'] = 0; break; } } if (!empty($frame['line'])) { ob_start(); debug_zval_dump($obj); self::$hashMask = (int) substr(ob_get_clean(), 17); } } self::$hashMask ^= hexdec(substr(spl_object_hash($obj), self::$hashOffset, PHP_INT_SIZE)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Cloner\DumperInterface; /** * Abstract mechanism for dumping a Data object. * * @author Nicolas Grekas */ abstract class AbstractDumper implements DataDumperInterface, DumperInterface { public static $defaultOutput = 'php://output'; protected $line = ''; protected $lineDumper; protected $outputStream; protected $decimalPoint; // This is locale dependent protected $indentPad = ' '; private $charset; /** * @param callable|resource|string|null $output A line dumper callable, an opened stream or an output path, defaults to static::$defaultOutput * @param string $charset The default character encoding to use for non-UTF8 strings */ public function __construct($output = null, $charset = null) { $this->setCharset($charset ?: ini_get('php.output_encoding') ?: ini_get('default_charset') ?: 'UTF-8'); $this->decimalPoint = (string) 0.5; $this->decimalPoint = $this->decimalPoint[1]; $this->setOutput($output ?: static::$defaultOutput); if (!$output && is_string(static::$defaultOutput)) { static::$defaultOutput = $this->outputStream; } } /** * Sets the output destination of the dumps. * * @param callable|resource|string $output A line dumper callable, an opened stream or an output path * * @return callable|resource|string The previous output destination */ public function setOutput($output) { $prev = null !== $this->outputStream ? $this->outputStream : $this->lineDumper; if (is_callable($output)) { $this->outputStream = null; $this->lineDumper = $output; } else { if (is_string($output)) { $output = fopen($output, 'wb'); } $this->outputStream = $output; $this->lineDumper = array($this, 'echoLine'); } return $prev; } /** * Sets the default character encoding to use for non-UTF8 strings. * * @param string $charset The default character encoding to use for non-UTF8 strings * * @return string The previous charset */ public function setCharset($charset) { $prev = $this->charset; $charset = strtoupper($charset); $charset = null === $charset || 'UTF-8' === $charset || 'UTF8' === $charset ? 'CP1252' : $charset; $this->charset = $charset; return $prev; } /** * Sets the indentation pad string. * * @param string $pad A string the will be prepended to dumped lines, repeated by nesting level * * @return string The indent pad */ public function setIndentPad($pad) { $prev = $this->indentPad; $this->indentPad = $pad; return $prev; } /** * Dumps a Data object. * * @param Data $data A Data object * @param callable|resource|string|null $output A line dumper callable, an opened stream or an output path */ public function dump(Data $data, $output = null) { $exception = null; if ($output) { $prevOutput = $this->setOutput($output); } try { $data->dump($this); $this->dumpLine(-1); } catch (\Exception $exception) { // Re-thrown below } catch (\Throwable $exception) { // Re-thrown below } if ($output) { $this->setOutput($prevOutput); } if (null !== $exception) { throw $exception; } } /** * Dumps the current line. * * @param int $depth The recursive depth in the dumped structure for the line being dumped */ protected function dumpLine($depth) { call_user_func($this->lineDumper, $this->line, $depth, $this->indentPad); $this->line = ''; } /** * Generic line dumper callback. * * @param string $line The line to write * @param int $depth The recursive depth in the dumped structure * @param string $indentPad The line indent pad */ protected function echoLine($line, $depth, $indentPad) { if (-1 !== $depth) { fwrite($this->outputStream, str_repeat($indentPad, $depth).$line."\n"); } } /** * Converts a non-UTF-8 string to UTF-8. * * @param string $s The non-UTF-8 string to convert * * @return string The string converted to UTF-8 */ protected function utf8Encode($s) { if (false !== $c = @iconv($this->charset, 'UTF-8', $s)) { return $c; } if ('CP1252' !== $this->charset && false !== $c = @iconv('CP1252', 'UTF-8', $s)) { return $c; } return iconv('CP850', 'UTF-8', $s); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper; use Symfony\Component\VarDumper\Cloner\Cursor; /** * CliDumper dumps variables for command line output. * * @author Nicolas Grekas */ class CliDumper extends AbstractDumper { public static $defaultColors; public static $defaultOutput = 'php://stdout'; protected $colors; protected $maxStringWidth = 0; protected $styles = array( // See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics 'default' => '38;5;208', 'num' => '1;38;5;38', 'const' => '1;38;5;208', 'str' => '1;38;5;113', 'note' => '38;5;38', 'ref' => '38;5;247', 'public' => '', 'protected' => '', 'private' => '', 'meta' => '38;5;170', 'key' => '38;5;113', 'index' => '38;5;38', ); protected static $controlCharsRx = '/[\x00-\x1F\x7F]+/'; protected static $controlCharsMap = array( "\t" => '\t', "\n" => '\n', "\v" => '\v', "\f" => '\f', "\r" => '\r', "\033" => '\e', ); /** * {@inheritdoc} */ public function __construct($output = null, $charset = null) { parent::__construct($output, $charset); if ('\\' === DIRECTORY_SEPARATOR && 'ON' !== @getenv('ConEmuANSI') && 'xterm' !== @getenv('TERM')) { // Use only the base 16 xterm colors when using ANSICON or standard Windows 10 CLI $this->setStyles(array( 'default' => '31', 'num' => '1;34', 'const' => '1;31', 'str' => '1;32', 'note' => '34', 'ref' => '1;30', 'meta' => '35', 'key' => '32', 'index' => '34', )); } } /** * Enables/disables colored output. * * @param bool $colors */ public function setColors($colors) { $this->colors = (bool) $colors; } /** * Sets the maximum number of characters per line for dumped strings. * * @param int $maxStringWidth */ public function setMaxStringWidth($maxStringWidth) { $this->maxStringWidth = (int) $maxStringWidth; } /** * Configures styles. * * @param array $styles A map of style names to style definitions */ public function setStyles(array $styles) { $this->styles = $styles + $this->styles; } /** * {@inheritdoc} */ public function dumpScalar(Cursor $cursor, $type, $value) { $this->dumpKey($cursor); $style = 'const'; $attr = array(); switch ($type) { case 'integer': $style = 'num'; break; case 'double': $style = 'num'; switch (true) { case INF === $value: $value = 'INF'; break; case -INF === $value: $value = '-INF'; break; case is_nan($value): $value = 'NAN'; break; default: $value = (string) $value; if (false === strpos($value, $this->decimalPoint)) { $value .= $this->decimalPoint.'0'; } break; } break; case 'NULL': $value = 'null'; break; case 'boolean': $value = $value ? 'true' : 'false'; break; default: $attr['value'] = isset($value[0]) && !preg_match('//u', $value) ? $this->utf8Encode($value) : $value; $value = isset($type[0]) && !preg_match('//u', $type) ? $this->utf8Encode($type) : $type; break; } $this->line .= $this->style($style, $value, $attr); $this->dumpLine($cursor->depth, true); } /** * {@inheritdoc} */ public function dumpString(Cursor $cursor, $str, $bin, $cut) { $this->dumpKey($cursor); if ($bin) { $str = $this->utf8Encode($str); } if ('' === $str) { $this->line .= '""'; $this->dumpLine($cursor->depth, true); } else { $attr = array( 'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0, 'binary' => $bin, ); $str = explode("\n", $str); if (isset($str[1]) && !isset($str[2]) && !isset($str[1][0])) { unset($str[1]); $str[0] .= "\n"; } $m = count($str) - 1; $i = $lineCut = 0; if ($bin) { $this->line .= 'b'; } if ($m) { $this->line .= '"""'; $this->dumpLine($cursor->depth); } else { $this->line .= '"'; } foreach ($str as $str) { if ($i < $m) { $str .= "\n"; } if (0 < $this->maxStringWidth && $this->maxStringWidth < $len = mb_strlen($str, 'UTF-8')) { $str = mb_substr($str, 0, $this->maxStringWidth, 'UTF-8'); $lineCut = $len - $this->maxStringWidth; } if ($m && 0 < $cursor->depth) { $this->line .= $this->indentPad; } if ('' !== $str) { $this->line .= $this->style('str', $str, $attr); } if ($i++ == $m) { if ($m) { if ('' !== $str) { $this->dumpLine($cursor->depth); if (0 < $cursor->depth) { $this->line .= $this->indentPad; } } $this->line .= '"""'; } else { $this->line .= '"'; } if ($cut < 0) { $this->line .= '…'; $lineCut = 0; } elseif ($cut) { $lineCut += $cut; } } if ($lineCut) { $this->line .= '…'.$lineCut; $lineCut = 0; } $this->dumpLine($cursor->depth, $i > $m); } } } /** * {@inheritdoc} */ public function enterHash(Cursor $cursor, $type, $class, $hasChild) { $this->dumpKey($cursor); if (!preg_match('//u', $class)) { $class = $this->utf8Encode($class); } if (Cursor::HASH_OBJECT === $type) { $prefix = $class && 'stdClass' !== $class ? $this->style('note', $class).' {' : '{'; } elseif (Cursor::HASH_RESOURCE === $type) { $prefix = $this->style('note', $class.' resource').($hasChild ? ' {' : ' '); } else { $prefix = $class ? $this->style('note', 'array:'.$class).' [' : '['; } if ($cursor->softRefCount || 0 < $cursor->softRefHandle) { $prefix .= $this->style('ref', (Cursor::HASH_RESOURCE === $type ? '@' : '#').(0 < $cursor->softRefHandle ? $cursor->softRefHandle : $cursor->softRefTo), array('count' => $cursor->softRefCount)); } elseif ($cursor->hardRefTo && !$cursor->refIndex && $class) { $prefix .= $this->style('ref', '&'.$cursor->hardRefTo, array('count' => $cursor->hardRefCount)); } elseif (!$hasChild && Cursor::HASH_RESOURCE === $type) { $prefix = substr($prefix, 0, -1); } $this->line .= $prefix; if ($hasChild) { $this->dumpLine($cursor->depth); } } /** * {@inheritdoc} */ public function leaveHash(Cursor $cursor, $type, $class, $hasChild, $cut) { $this->dumpEllipsis($cursor, $hasChild, $cut); $this->line .= Cursor::HASH_OBJECT === $type ? '}' : (Cursor::HASH_RESOURCE !== $type ? ']' : ($hasChild ? '}' : '')); $this->dumpLine($cursor->depth, true); } /** * Dumps an ellipsis for cut children. * * @param Cursor $cursor The Cursor position in the dump * @param bool $hasChild When the dump of the hash has child item * @param int $cut The number of items the hash has been cut by */ protected function dumpEllipsis(Cursor $cursor, $hasChild, $cut) { if ($cut) { $this->line .= ' …'; if (0 < $cut) { $this->line .= $cut; } if ($hasChild) { $this->dumpLine($cursor->depth + 1); } } } /** * Dumps a key in a hash structure. * * @param Cursor $cursor The Cursor position in the dump */ protected function dumpKey(Cursor $cursor) { if (null !== $key = $cursor->hashKey) { if ($cursor->hashKeyIsBinary) { $key = $this->utf8Encode($key); } $attr = array('binary' => $cursor->hashKeyIsBinary); $bin = $cursor->hashKeyIsBinary ? 'b' : ''; $style = 'key'; switch ($cursor->hashType) { default: case Cursor::HASH_INDEXED: $style = 'index'; case Cursor::HASH_ASSOC: if (is_int($key)) { $this->line .= $this->style($style, $key).' => '; } else { $this->line .= $bin.'"'.$this->style($style, $key).'" => '; } break; case Cursor::HASH_RESOURCE: $key = "\0~\0".$key; // No break; case Cursor::HASH_OBJECT: if (!isset($key[0]) || "\0" !== $key[0]) { $this->line .= '+'.$bin.$this->style('public', $key).': '; } elseif (0 < strpos($key, "\0", 1)) { $key = explode("\0", substr($key, 1), 2); switch ($key[0]) { case '+': // User inserted keys $attr['dynamic'] = true; $this->line .= '+'.$bin.'"'.$this->style('public', $key[1], $attr).'": '; break 2; case '~': $style = 'meta'; break; case '*': $style = 'protected'; $bin = '#'.$bin; break; default: $attr['class'] = $key[0]; $style = 'private'; $bin = '-'.$bin; break; } $this->line .= $bin.$this->style($style, $key[1], $attr).': '; } else { // This case should not happen $this->line .= '-'.$bin.'"'.$this->style('private', $key, array('class' => '')).'": '; } break; } if ($cursor->hardRefTo) { $this->line .= $this->style('ref', '&'.($cursor->hardRefCount ? $cursor->hardRefTo : ''), array('count' => $cursor->hardRefCount)).' '; } } } /** * Decorates a value with some style. * * @param string $style The type of style being applied * @param string $value The value being styled * @param array $attr Optional context information * * @return string The value with style decoration */ protected function style($style, $value, $attr = array()) { if (null === $this->colors) { $this->colors = $this->supportsColors(); } $style = $this->styles[$style]; $map = static::$controlCharsMap; $startCchr = $this->colors ? "\033[m\033[{$this->styles['default']}m" : ''; $endCchr = $this->colors ? "\033[m\033[{$style}m" : ''; $value = preg_replace_callback(static::$controlCharsRx, function ($c) use ($map, $startCchr, $endCchr) { $s = $startCchr; $c = $c[$i = 0]; do { $s .= isset($map[$c[$i]]) ? $map[$c[$i]] : sprintf('\x%02X', ord($c[$i])); } while (isset($c[++$i])); return $s.$endCchr; }, $value, -1, $cchrCount); if ($this->colors) { if ($cchrCount && "\033" === $value[0]) { $value = substr($value, strlen($startCchr)); } else { $value = "\033[{$style}m".$value; } if ($cchrCount && $endCchr === substr($value, -strlen($endCchr))) { $value = substr($value, 0, -strlen($endCchr)); } else { $value .= "\033[{$this->styles['default']}m"; } } return $value; } /** * @return bool Tells if the current output stream supports ANSI colors or not */ protected function supportsColors() { if ($this->outputStream !== static::$defaultOutput) { return @(is_resource($this->outputStream) && function_exists('posix_isatty') && posix_isatty($this->outputStream)); } if (null !== static::$defaultColors) { return static::$defaultColors; } if (isset($_SERVER['argv'][1])) { $colors = $_SERVER['argv']; $i = count($colors); while (--$i > 0) { if (isset($colors[$i][5])) { switch ($colors[$i]) { case '--ansi': case '--color': case '--color=yes': case '--color=force': case '--color=always': return static::$defaultColors = true; case '--no-ansi': case '--color=no': case '--color=none': case '--color=never': return static::$defaultColors = false; } } } } if ('\\' === DIRECTORY_SEPARATOR) { static::$defaultColors = @( '10.0.10586' === PHP_WINDOWS_VERSION_MAJOR.'.'.PHP_WINDOWS_VERSION_MINOR.'.'.PHP_WINDOWS_VERSION_BUILD || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM') ); } elseif (function_exists('posix_isatty')) { $h = stream_get_meta_data($this->outputStream) + array('wrapper_type' => null); $h = 'Output' === $h['stream_type'] && 'PHP' === $h['wrapper_type'] ? fopen('php://stdout', 'wb') : $this->outputStream; static::$defaultColors = @posix_isatty($h); } else { static::$defaultColors = false; } return static::$defaultColors; } /** * {@inheritdoc} */ protected function dumpLine($depth, $endOfValue = false) { if ($this->colors) { $this->line = sprintf("\033[%sm%s\033[m", $this->styles['default'], $this->line); } parent::dumpLine($depth); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper; use Symfony\Component\VarDumper\Cloner\Data; /** * DataDumperInterface for dumping Data objects. * * @author Nicolas Grekas */ interface DataDumperInterface { /** * Dumps a Data object. * * @param Data $data A Data object */ public function dump(Data $data); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Dumper; use Symfony\Component\VarDumper\Cloner\Cursor; use Symfony\Component\VarDumper\Cloner\Data; /** * HtmlDumper dumps variables as HTML. * * @author Nicolas Grekas */ class HtmlDumper extends CliDumper { public static $defaultOutput = 'php://output'; protected $dumpHeader; protected $dumpPrefix = '
    ';
        protected $dumpSuffix = '
    '; protected $dumpId = 'sf-dump'; protected $colors = true; protected $headerIsDumped = false; protected $lastDepth = -1; protected $styles = array( 'default' => 'background-color:#18171B; color:#FF8400; line-height:1.2em; font:12px Menlo, Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: normal', 'num' => 'font-weight:bold; color:#1299DA', 'const' => 'font-weight:bold', 'str' => 'font-weight:bold; color:#56DB3A', 'note' => 'color:#1299DA', 'ref' => 'color:#A0A0A0', 'public' => 'color:#FFFFFF', 'protected' => 'color:#FFFFFF', 'private' => 'color:#FFFFFF', 'meta' => 'color:#B729D9', 'key' => 'color:#56DB3A', 'index' => 'color:#1299DA', ); /** * {@inheritdoc} */ public function __construct($output = null, $charset = null) { AbstractDumper::__construct($output, $charset); $this->dumpId = 'sf-dump-'.mt_rand(); } /** * {@inheritdoc} */ public function setStyles(array $styles) { $this->headerIsDumped = false; $this->styles = $styles + $this->styles; } /** * Sets an HTML header that will be dumped once in the output stream. * * @param string $header An HTML string */ public function setDumpHeader($header) { $this->dumpHeader = $header; } /** * Sets an HTML prefix and suffix that will encapse every single dump. * * @param string $prefix The prepended HTML string * @param string $suffix The appended HTML string */ public function setDumpBoundaries($prefix, $suffix) { $this->dumpPrefix = $prefix; $this->dumpSuffix = $suffix; } /** * {@inheritdoc} */ public function dump(Data $data, $output = null) { parent::dump($data, $output); $this->dumpId = 'sf-dump-'.mt_rand(); } /** * Dumps the HTML header. */ protected function getDumpHeader() { $this->headerIsDumped = null !== $this->outputStream ? $this->outputStream : $this->lineDumper; if (null !== $this->dumpHeader) { return $this->dumpHeader; } $line = <<<'EOHTML' '.$this->dumpHeader; } /** * {@inheritdoc} */ public function enterHash(Cursor $cursor, $type, $class, $hasChild) { parent::enterHash($cursor, $type, $class, false); if ($hasChild) { if ($cursor->refIndex) { $r = Cursor::HASH_OBJECT !== $type ? 1 - (Cursor::HASH_RESOURCE !== $type) : 2; $r .= $r && 0 < $cursor->softRefHandle ? $cursor->softRefHandle : $cursor->refIndex; $this->line .= sprintf('', $this->dumpId, $r); } else { $this->line .= ''; } $this->dumpLine($cursor->depth); } } /** * {@inheritdoc} */ public function leaveHash(Cursor $cursor, $type, $class, $hasChild, $cut) { $this->dumpEllipsis($cursor, $hasChild, $cut); if ($hasChild) { $this->line .= ''; } parent::leaveHash($cursor, $type, $class, $hasChild, 0); } /** * {@inheritdoc} */ protected function style($style, $value, $attr = array()) { if ('' === $value) { return ''; } $v = esc($value); if ('ref' === $style) { if (empty($attr['count'])) { return sprintf('%s', $v); } $r = ('#' !== $v[0] ? 1 - ('@' !== $v[0]) : 2).substr($value, 1); return sprintf('%s', $this->dumpId, $r, 1 + $attr['count'], $v); } if ('const' === $style && isset($attr['value'])) { $style .= sprintf(' title="%s"', esc(is_scalar($attr['value']) ? $attr['value'] : json_encode($attr['value']))); } elseif ('public' === $style) { $style .= sprintf(' title="%s"', empty($attr['dynamic']) ? 'Public property' : 'Runtime added dynamic property'); } elseif ('str' === $style && 1 < $attr['length']) { $style .= sprintf(' title="%d%s characters"', $attr['length'], $attr['binary'] ? ' binary or non-UTF-8' : ''); } elseif ('note' === $style && false !== $c = strrpos($v, '\\')) { return sprintf('%s', $v, $style, substr($v, $c + 1)); } elseif ('protected' === $style) { $style .= ' title="Protected property"'; } elseif ('private' === $style) { $style .= sprintf(' title="Private property defined in class: `%s`"', esc($attr['class'])); } $map = static::$controlCharsMap; $style = ""; $v = preg_replace_callback(static::$controlCharsRx, function ($c) use ($map, $style) { $s = ''; $c = $c[$i = 0]; do { $s .= isset($map[$c[$i]]) ? $map[$c[$i]] : sprintf('\x%02X', ord($c[$i])); } while (isset($c[++$i])); return $s.$style; }, $v, -1, $cchrCount); if ($cchrCount && '<' === $v[0]) { $v = substr($v, 7); } else { $v = $style.$v; } if ($cchrCount && '>' === substr($v, -1)) { $v = substr($v, 0, -strlen($style)); } else { $v .= ''; } return $v; } /** * {@inheritdoc} */ protected function dumpLine($depth, $endOfValue = false) { if (-1 === $this->lastDepth) { $this->line = sprintf($this->dumpPrefix, $this->dumpId, $this->indentPad).$this->line; } if ($this->headerIsDumped !== (null !== $this->outputStream ? $this->outputStream : $this->lineDumper)) { $this->line = $this->getDumpHeader().$this->line; } if (-1 === $depth) { $this->line .= sprintf($this->dumpSuffix, $this->dumpId); } $this->lastDepth = $depth; $this->line = mb_convert_encoding($this->line, 'HTML-ENTITIES', 'UTF-8'); if (-1 === $depth) { AbstractDumper::dumpLine(0); } AbstractDumper::dumpLine($depth); } } function esc($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper\Exception; /** * @author Nicolas Grekas */ class ThrowingCasterException extends \Exception { /** * @param \Exception $prev The exception thrown from the caster */ public function __construct($prev, \Exception $e = null) { if (!$prev instanceof \Exception) { @trigger_error('Providing $caster as the first argument of the '.__METHOD__.' method is deprecated since version 2.8 and will be removed in 3.0. Provide directly the $prev exception instead.', E_USER_DEPRECATED); $prev = $e; } parent::__construct('Unexpected '.get_class($prev).' thrown from a caster: '.$prev->getMessage(), 0, $prev); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ use Symfony\Component\VarDumper\VarDumper; if (!function_exists('dump')) { /** * @author Nicolas Grekas */ function dump($var) { foreach (func_get_args() as $var) { VarDumper::dump($var); } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\VarDumper; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Dumper\HtmlDumper; // Load the global dump() function require_once __DIR__.'/Resources/functions/dump.php'; /** * @author Nicolas Grekas */ class VarDumper { private static $handler; public static function dump($var) { if (null === self::$handler) { $cloner = new VarCloner(); $dumper = 'cli' === PHP_SAPI ? new CliDumper() : new HtmlDumper(); self::$handler = function ($var) use ($cloner, $dumper) { $dumper->dump($cloner->cloneVar($var)); }; } return call_user_func(self::$handler, $var); } public static function setHandler($callable) { if (null !== $callable && !is_callable($callable, true)) { throw new \InvalidArgumentException('Invalid PHP callback.'); } $prevHandler = self::$handler; self::$handler = $callable; return $prevHandler; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; /** * Dumper dumps PHP variables to YAML strings. * * @author Fabien Potencier */ class Dumper { /** * The amount of spaces to use for indentation of nested nodes. * * @var int */ protected $indentation = 4; /** * Sets the indentation. * * @param int $num The amount of spaces to use for indentation of nested nodes */ public function setIndentation($num) { if ($num < 1) { throw new \InvalidArgumentException('The indentation must be greater than zero.'); } $this->indentation = (int) $num; } /** * Dumps a PHP value to YAML. * * @param mixed $input The PHP value * @param int $inline The level where you switch to inline YAML * @param int $indent The level of indentation (used internally) * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise * @param bool $objectSupport true if object support is enabled, false otherwise * * @return string The YAML representation of the PHP value */ public function dump($input, $inline = 0, $indent = 0, $exceptionOnInvalidType = false, $objectSupport = false) { $output = ''; $prefix = $indent ? str_repeat(' ', $indent) : ''; if ($inline <= 0 || !is_array($input) || empty($input)) { $output .= $prefix.Inline::dump($input, $exceptionOnInvalidType, $objectSupport); } else { $isAHash = Inline::isHash($input); foreach ($input as $key => $value) { $willBeInlined = $inline - 1 <= 0 || !is_array($value) || empty($value); $output .= sprintf('%s%s%s%s', $prefix, $isAHash ? Inline::dump($key, $exceptionOnInvalidType, $objectSupport).':' : '-', $willBeInlined ? ' ' : "\n", $this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $exceptionOnInvalidType, $objectSupport) ).($willBeInlined ? "\n" : ''); } } return $output; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; /** * Escaper encapsulates escaping rules for single and double-quoted * YAML strings. * * @author Matthew Lewinski * * @internal */ class Escaper { // Characters that would cause a dumped string to require double quoting. const REGEX_CHARACTER_TO_ESCAPE = "[\\x00-\\x1f]|\xc2\x85|\xc2\xa0|\xe2\x80\xa8|\xe2\x80\xa9"; // Mapping arrays for escaping a double quoted string. The backslash is // first to ensure proper escaping because str_replace operates iteratively // on the input arrays. This ordering of the characters avoids the use of strtr, // which performs more slowly. private static $escapees = array('\\', '\\\\', '\\"', '"', "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08", "\x09", "\x0a", "\x0b", "\x0c", "\x0d", "\x0e", "\x0f", "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17", "\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f", "\xc2\x85", "\xc2\xa0", "\xe2\x80\xa8", "\xe2\x80\xa9"); private static $escaped = array('\\\\', '\\"', '\\\\', '\\"', '\\0', '\\x01', '\\x02', '\\x03', '\\x04', '\\x05', '\\x06', '\\a', '\\b', '\\t', '\\n', '\\v', '\\f', '\\r', '\\x0e', '\\x0f', '\\x10', '\\x11', '\\x12', '\\x13', '\\x14', '\\x15', '\\x16', '\\x17', '\\x18', '\\x19', '\\x1a', '\\e', '\\x1c', '\\x1d', '\\x1e', '\\x1f', '\\N', '\\_', '\\L', '\\P'); /** * Determines if a PHP value would require double quoting in YAML. * * @param string $value A PHP value * * @return bool True if the value would require double quotes */ public static function requiresDoubleQuoting($value) { return preg_match('/'.self::REGEX_CHARACTER_TO_ESCAPE.'/u', $value); } /** * Escapes and surrounds a PHP value with double quotes. * * @param string $value A PHP value * * @return string The quoted, escaped string */ public static function escapeWithDoubleQuotes($value) { return sprintf('"%s"', str_replace(self::$escapees, self::$escaped, $value)); } /** * Determines if a PHP value would require single quoting in YAML. * * @param string $value A PHP value * * @return bool True if the value would require single quotes */ public static function requiresSingleQuoting($value) { // Determines if a PHP value is entirely composed of a value that would // require single quoting in YAML. if (in_array(strtolower($value), array('null', '~', 'true', 'false', 'y', 'n', 'yes', 'no', 'on', 'off'))) { return true; } // Determines if the PHP value contains any single characters that would // cause it to require single quoting in YAML. return preg_match('/[ \s \' " \: \{ \} \[ \] , & \* \# \?] | \A[ \- ? | < > = ! % @ ` ]/x', $value); } /** * Escapes and surrounds a PHP value with single quotes. * * @param string $value A PHP value * * @return string The quoted, escaped string */ public static function escapeWithSingleQuotes($value) { return sprintf("'%s'", str_replace('\'', '\'\'', $value)); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Exception; /** * Exception class thrown when an error occurs during dumping. * * @author Fabien Potencier */ class DumpException extends RuntimeException { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Exception; /** * Exception interface for all exceptions thrown by the component. * * @author Fabien Potencier */ interface ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Exception; /** * Exception class thrown when an error occurs during parsing. * * @author Fabien Potencier */ class ParseException extends RuntimeException { private $parsedFile; private $parsedLine; private $snippet; private $rawMessage; /** * Constructor. * * @param string $message The error message * @param int $parsedLine The line where the error occurred * @param int $snippet The snippet of code near the problem * @param string $parsedFile The file name where the error occurred * @param \Exception $previous The previous exception */ public function __construct($message, $parsedLine = -1, $snippet = null, $parsedFile = null, \Exception $previous = null) { $this->parsedFile = $parsedFile; $this->parsedLine = $parsedLine; $this->snippet = $snippet; $this->rawMessage = $message; $this->updateRepr(); parent::__construct($this->message, 0, $previous); } /** * Gets the snippet of code near the error. * * @return string The snippet of code */ public function getSnippet() { return $this->snippet; } /** * Sets the snippet of code near the error. * * @param string $snippet The code snippet */ public function setSnippet($snippet) { $this->snippet = $snippet; $this->updateRepr(); } /** * Gets the filename where the error occurred. * * This method returns null if a string is parsed. * * @return string The filename */ public function getParsedFile() { return $this->parsedFile; } /** * Sets the filename where the error occurred. * * @param string $parsedFile The filename */ public function setParsedFile($parsedFile) { $this->parsedFile = $parsedFile; $this->updateRepr(); } /** * Gets the line where the error occurred. * * @return int The file line */ public function getParsedLine() { return $this->parsedLine; } /** * Sets the line where the error occurred. * * @param int $parsedLine The file line */ public function setParsedLine($parsedLine) { $this->parsedLine = $parsedLine; $this->updateRepr(); } private function updateRepr() { $this->message = $this->rawMessage; $dot = false; if ('.' === substr($this->message, -1)) { $this->message = substr($this->message, 0, -1); $dot = true; } if (null !== $this->parsedFile) { if (PHP_VERSION_ID >= 50400) { $jsonOptions = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; } else { $jsonOptions = 0; } $this->message .= sprintf(' in %s', json_encode($this->parsedFile, $jsonOptions)); } if ($this->parsedLine >= 0) { $this->message .= sprintf(' at line %d', $this->parsedLine); } if ($this->snippet) { $this->message .= sprintf(' (near "%s")', $this->snippet); } if ($dot) { $this->message .= '.'; } } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml\Exception; /** * Exception class thrown when an error occurs during parsing. * * @author Romain Neutron */ class RuntimeException extends \RuntimeException implements ExceptionInterface { } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Exception\DumpException; /** * Inline implements a YAML parser/dumper for the YAML inline syntax. * * @author Fabien Potencier */ class Inline { const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\']*(?:\'\'[^\']*)*)\')'; private static $exceptionOnInvalidType = false; private static $objectSupport = false; private static $objectForMap = false; /** * Converts a YAML string to a PHP value. * * @param string $value A YAML string * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise * @param bool $objectSupport true if object support is enabled, false otherwise * @param bool $objectForMap true if maps should return a stdClass instead of array() * @param array $references Mapping of variable names to values * * @return mixed A PHP value * * @throws ParseException */ public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false, $references = array()) { self::$exceptionOnInvalidType = $exceptionOnInvalidType; self::$objectSupport = $objectSupport; self::$objectForMap = $objectForMap; $value = trim($value); if ('' === $value) { return ''; } if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) { $mbEncoding = mb_internal_encoding(); mb_internal_encoding('ASCII'); } $i = 0; switch ($value[0]) { case '[': $result = self::parseSequence($value, $i, $references); ++$i; break; case '{': $result = self::parseMapping($value, $i, $references); ++$i; break; default: $result = self::parseScalar($value, null, array('"', "'"), $i, true, $references); } // some comments are allowed at the end if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) { throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i))); } if (isset($mbEncoding)) { mb_internal_encoding($mbEncoding); } return $result; } /** * Dumps a given PHP variable to a YAML string. * * @param mixed $value The PHP variable to convert * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise * @param bool $objectSupport true if object support is enabled, false otherwise * * @return string The YAML string representing the PHP value * * @throws DumpException When trying to dump PHP resource */ public static function dump($value, $exceptionOnInvalidType = false, $objectSupport = false) { switch (true) { case is_resource($value): if ($exceptionOnInvalidType) { throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value))); } return 'null'; case is_object($value): if ($objectSupport) { return '!php/object:'.serialize($value); } if ($exceptionOnInvalidType) { throw new DumpException('Object support when dumping a YAML file has been disabled.'); } return 'null'; case is_array($value): return self::dumpArray($value, $exceptionOnInvalidType, $objectSupport); case null === $value: return 'null'; case true === $value: return 'true'; case false === $value: return 'false'; case ctype_digit($value): return is_string($value) ? "'$value'" : (int) $value; case is_numeric($value): $locale = setlocale(LC_NUMERIC, 0); if (false !== $locale) { setlocale(LC_NUMERIC, 'C'); } if (is_float($value)) { $repr = (string) $value; if (is_infinite($value)) { $repr = str_ireplace('INF', '.Inf', $repr); } elseif (floor($value) == $value && $repr == $value) { // Preserve float data type since storing a whole number will result in integer value. $repr = '!!float '.$repr; } } else { $repr = is_string($value) ? "'$value'" : (string) $value; } if (false !== $locale) { setlocale(LC_NUMERIC, $locale); } return $repr; case '' == $value: return "''"; case Escaper::requiresDoubleQuoting($value): return Escaper::escapeWithDoubleQuotes($value); case Escaper::requiresSingleQuoting($value): case preg_match(self::getHexRegex(), $value): case preg_match(self::getTimestampRegex(), $value): return Escaper::escapeWithSingleQuotes($value); default: return $value; } } /** * Check if given array is hash or just normal indexed array. * * @internal * * @param array $value The PHP array to check * * @return bool true if value is hash array, false otherwise */ public static function isHash(array $value) { $expectedKey = 0; foreach ($value as $key => $val) { if ($key !== $expectedKey++) { return true; } } return false; } /** * Dumps a PHP array to a YAML string. * * @param array $value The PHP array to dump * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise * @param bool $objectSupport true if object support is enabled, false otherwise * * @return string The YAML string representing the PHP array */ private static function dumpArray($value, $exceptionOnInvalidType, $objectSupport) { // array if ($value && !self::isHash($value)) { $output = array(); foreach ($value as $val) { $output[] = self::dump($val, $exceptionOnInvalidType, $objectSupport); } return sprintf('[%s]', implode(', ', $output)); } // hash $output = array(); foreach ($value as $key => $val) { $output[] = sprintf('%s: %s', self::dump($key, $exceptionOnInvalidType, $objectSupport), self::dump($val, $exceptionOnInvalidType, $objectSupport)); } return sprintf('{ %s }', implode(', ', $output)); } /** * Parses a YAML scalar. * * @param string $scalar * @param string $delimiters * @param array $stringDelimiters * @param int &$i * @param bool $evaluate * @param array $references * * @return string * * @throws ParseException When malformed inline YAML string is parsed * * @internal */ public static function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true, $references = array()) { if (in_array($scalar[$i], $stringDelimiters)) { // quoted scalar $output = self::parseQuotedScalar($scalar, $i); if (null !== $delimiters) { $tmp = ltrim(substr($scalar, $i), ' '); if (!in_array($tmp[0], $delimiters)) { throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i))); } } } else { // "normal" string if (!$delimiters) { $output = substr($scalar, $i); $i += strlen($output); // remove comments if (preg_match('/[ \t]+#/', $output, $match, PREG_OFFSET_CAPTURE)) { $output = substr($output, 0, $match[0][1]); } } elseif (preg_match('/^(.+?)('.implode('|', $delimiters).')/', substr($scalar, $i), $match)) { $output = $match[1]; $i += strlen($output); } else { throw new ParseException(sprintf('Malformed inline YAML string: %s.', $scalar)); } // a non-quoted string cannot start with @ or ` (reserved) nor with a scalar indicator (| or >) if ($output && ('@' === $output[0] || '`' === $output[0] || '|' === $output[0] || '>' === $output[0])) { @trigger_error(sprintf('Not quoting the scalar "%s" starting with "%s" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $output, $output[0]), E_USER_DEPRECATED); // to be thrown in 3.0 // throw new ParseException(sprintf('The reserved indicator "%s" cannot start a plain scalar; you need to quote the scalar.', $output[0])); } if ($evaluate) { $output = self::evaluateScalar($output, $references); } } return $output; } /** * Parses a YAML quoted scalar. * * @param string $scalar * @param int &$i * * @return string * * @throws ParseException When malformed inline YAML string is parsed */ private static function parseQuotedScalar($scalar, &$i) { if (!preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) { throw new ParseException(sprintf('Malformed inline YAML string: %s.', substr($scalar, $i))); } $output = substr($match[0], 1, strlen($match[0]) - 2); $unescaper = new Unescaper(); if ('"' == $scalar[$i]) { $output = $unescaper->unescapeDoubleQuotedString($output); } else { $output = $unescaper->unescapeSingleQuotedString($output); } $i += strlen($match[0]); return $output; } /** * Parses a YAML sequence. * * @param string $sequence * @param int &$i * @param array $references * * @return array * * @throws ParseException When malformed inline YAML string is parsed */ private static function parseSequence($sequence, &$i = 0, $references = array()) { $output = array(); $len = strlen($sequence); ++$i; // [foo, bar, ...] while ($i < $len) { switch ($sequence[$i]) { case '[': // nested sequence $output[] = self::parseSequence($sequence, $i, $references); break; case '{': // nested mapping $output[] = self::parseMapping($sequence, $i, $references); break; case ']': return $output; case ',': case ' ': break; default: $isQuoted = in_array($sequence[$i], array('"', "'")); $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i, true, $references); // the value can be an array if a reference has been resolved to an array var if (!is_array($value) && !$isQuoted && false !== strpos($value, ': ')) { // embedded mapping? try { $pos = 0; $value = self::parseMapping('{'.$value.'}', $pos, $references); } catch (\InvalidArgumentException $e) { // no, it's not } } $output[] = $value; --$i; } ++$i; } throw new ParseException(sprintf('Malformed inline YAML string: %s.', $sequence)); } /** * Parses a YAML mapping. * * @param string $mapping * @param int &$i * @param array $references * * @return array|\stdClass * * @throws ParseException When malformed inline YAML string is parsed */ private static function parseMapping($mapping, &$i = 0, $references = array()) { $output = array(); $len = strlen($mapping); ++$i; // {foo: bar, bar:foo, ...} while ($i < $len) { switch ($mapping[$i]) { case ' ': case ',': ++$i; continue 2; case '}': if (self::$objectForMap) { return (object) $output; } return $output; } // key $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false); // value $done = false; while ($i < $len) { switch ($mapping[$i]) { case '[': // nested sequence $value = self::parseSequence($mapping, $i, $references); // Spec: Keys MUST be unique; first one wins. // Parser cannot abort this mapping earlier, since lines // are processed sequentially. if (!isset($output[$key])) { $output[$key] = $value; } $done = true; break; case '{': // nested mapping $value = self::parseMapping($mapping, $i, $references); // Spec: Keys MUST be unique; first one wins. // Parser cannot abort this mapping earlier, since lines // are processed sequentially. if (!isset($output[$key])) { $output[$key] = $value; } $done = true; break; case ':': case ' ': break; default: $value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i, true, $references); // Spec: Keys MUST be unique; first one wins. // Parser cannot abort this mapping earlier, since lines // are processed sequentially. if (!isset($output[$key])) { $output[$key] = $value; } $done = true; --$i; } ++$i; if ($done) { continue 2; } } } throw new ParseException(sprintf('Malformed inline YAML string: %s.', $mapping)); } /** * Evaluates scalars and replaces magic values. * * @param string $scalar * @param array $references * * @return string A YAML string * * @throws ParseException when object parsing support was disabled and the parser detected a PHP object or when a reference could not be resolved */ private static function evaluateScalar($scalar, $references = array()) { $scalar = trim($scalar); $scalarLower = strtolower($scalar); if (0 === strpos($scalar, '*')) { if (false !== $pos = strpos($scalar, '#')) { $value = substr($scalar, 1, $pos - 2); } else { $value = substr($scalar, 1); } // an unquoted * if (false === $value || '' === $value) { throw new ParseException('A reference must contain at least one character.'); } if (!array_key_exists($value, $references)) { throw new ParseException(sprintf('Reference "%s" does not exist.', $value)); } return $references[$value]; } switch (true) { case 'null' === $scalarLower: case '' === $scalar: case '~' === $scalar: return; case 'true' === $scalarLower: return true; case 'false' === $scalarLower: return false; // Optimise for returning strings. case $scalar[0] === '+' || $scalar[0] === '-' || $scalar[0] === '.' || $scalar[0] === '!' || is_numeric($scalar[0]): switch (true) { case 0 === strpos($scalar, '!str'): return (string) substr($scalar, 5); case 0 === strpos($scalar, '! '): return (int) self::parseScalar(substr($scalar, 2)); case 0 === strpos($scalar, '!php/object:'): if (self::$objectSupport) { return unserialize(substr($scalar, 12)); } if (self::$exceptionOnInvalidType) { throw new ParseException('Object support when parsing a YAML file has been disabled.'); } return; case 0 === strpos($scalar, '!!php/object:'): if (self::$objectSupport) { return unserialize(substr($scalar, 13)); } if (self::$exceptionOnInvalidType) { throw new ParseException('Object support when parsing a YAML file has been disabled.'); } return; case 0 === strpos($scalar, '!!float '): return (float) substr($scalar, 8); case ctype_digit($scalar): $raw = $scalar; $cast = (int) $scalar; return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw); case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)): $raw = $scalar; $cast = (int) $scalar; return '0' == $scalar[1] ? octdec($scalar) : (((string) $raw === (string) $cast) ? $cast : $raw); case is_numeric($scalar): case preg_match(self::getHexRegex(), $scalar): return '0x' === $scalar[0].$scalar[1] ? hexdec($scalar) : (float) $scalar; case '.inf' === $scalarLower: case '.nan' === $scalarLower: return -log(0); case '-.inf' === $scalarLower: return log(0); case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar): return (float) str_replace(',', '', $scalar); case preg_match(self::getTimestampRegex(), $scalar): $timeZone = date_default_timezone_get(); date_default_timezone_set('UTC'); $time = strtotime($scalar); date_default_timezone_set($timeZone); return $time; } default: return (string) $scalar; } } /** * Gets a regex that matches a YAML date. * * @return string The regular expression * * @see http://www.yaml.org/spec/1.2/spec.html#id2761573 */ private static function getTimestampRegex() { return <<[0-9][0-9][0-9][0-9]) -(?P[0-9][0-9]?) -(?P[0-9][0-9]?) (?:(?:[Tt]|[ \t]+) (?P[0-9][0-9]?) :(?P[0-9][0-9]) :(?P[0-9][0-9]) (?:\.(?P[0-9]*))? (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?) (?::(?P[0-9][0-9]))?))?)? $~x EOF; } /** * Gets a regex that matches a YAML number in hexadecimal notation. * * @return string */ private static function getHexRegex() { return '~^0x[0-9a-f]++$~i'; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; use Symfony\Component\Yaml\Exception\ParseException; /** * Parser parses YAML strings to convert them to PHP arrays. * * @author Fabien Potencier */ class Parser { const BLOCK_SCALAR_HEADER_PATTERN = '(?P\||>)(?P\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P +#.*)?'; // BC - wrongly named const FOLDED_SCALAR_PATTERN = self::BLOCK_SCALAR_HEADER_PATTERN; private $offset = 0; private $totalNumberOfLines; private $lines = array(); private $currentLineNb = -1; private $currentLine = ''; private $refs = array(); private $skippedLineNumbers = array(); private $locallySkippedLineNumbers = array(); /** * Constructor. * * @param int $offset The offset of YAML document (used for line numbers in error messages) * @param int|null $totalNumberOfLines The overall number of lines being parsed * @param int[] $skippedLineNumbers Number of comment lines that have been skipped by the parser */ public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array()) { $this->offset = $offset; $this->totalNumberOfLines = $totalNumberOfLines; $this->skippedLineNumbers = $skippedLineNumbers; } /** * Parses a YAML string to a PHP value. * * @param string $value A YAML string * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise * @param bool $objectSupport true if object support is enabled, false otherwise * @param bool $objectForMap true if maps should return a stdClass instead of array() * * @return mixed A PHP value * * @throws ParseException If the YAML is not valid */ public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false) { if (!preg_match('//u', $value)) { throw new ParseException('The YAML value does not appear to be valid UTF-8.'); } $this->currentLineNb = -1; $this->currentLine = ''; $value = $this->cleanup($value); $this->lines = explode("\n", $value); if (null === $this->totalNumberOfLines) { $this->totalNumberOfLines = count($this->lines); } if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) { $mbEncoding = mb_internal_encoding(); mb_internal_encoding('UTF-8'); } $data = array(); $context = null; $allowOverwrite = false; while ($this->moveToNextLine()) { if ($this->isCurrentLineEmpty()) { continue; } // tab? if ("\t" === $this->currentLine[0]) { throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } $isRef = $mergeNode = false; if (preg_match('#^\-((?P\s+)(?P.+?))?\s*$#u', $this->currentLine, $values)) { if ($context && 'mapping' == $context) { throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine); } $context = 'sequence'; if (isset($values['value']) && preg_match('#^&(?P[^ ]+) *(?P.*)#u', $values['value'], $matches)) { $isRef = $matches['ref']; $values['value'] = $matches['value']; } // array if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap); } else { if (isset($values['leadspaces']) && preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P.+?))?\s*$#u', $values['value'], $matches) ) { // this is a compact notation element, add to next block and parse $block = $values['value']; if ($this->isNextLineIndented()) { $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1); } $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap); } else { $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context); } } if ($isRef) { $this->refs[$isRef] = end($data); } } elseif (preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P.+?))?\s*$#u', $this->currentLine, $values) && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))) { if ($context && 'sequence' == $context) { throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine); } $context = 'mapping'; // force correct settings Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); try { $key = Inline::parseScalar($values['key']); } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); throw $e; } // Convert float keys to strings, to avoid being converted to integers by PHP if (is_float($key)) { $key = (string) $key; } if ('<<' === $key) { $mergeNode = true; $allowOverwrite = true; if (isset($values['value']) && 0 === strpos($values['value'], '*')) { $refName = substr($values['value'], 1); if (!array_key_exists($refName, $this->refs)) { throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine); } $refValue = $this->refs[$refName]; if (!is_array($refValue)) { throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } foreach ($refValue as $key => $value) { if (!isset($data[$key])) { $data[$key] = $value; } } } else { if (isset($values['value']) && $values['value'] !== '') { $value = $values['value']; } else { $value = $this->getNextEmbedBlock(); } $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap); if (!is_array($parsed)) { throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } if (isset($parsed[0])) { // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier // in the sequence override keys specified in later mapping nodes. foreach ($parsed as $parsedItem) { if (!is_array($parsedItem)) { throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem); } foreach ($parsedItem as $key => $value) { if (!isset($data[$key])) { $data[$key] = $value; } } } } else { // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the // current mapping, unless the key already exists in it. foreach ($parsed as $key => $value) { if (!isset($data[$key])) { $data[$key] = $value; } } } } } elseif (isset($values['value']) && preg_match('#^&(?P[^ ]+) *(?P.*)#u', $values['value'], $matches)) { $isRef = $matches['ref']; $values['value'] = $matches['value']; } if ($mergeNode) { // Merge keys } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { // hash // if next line is less indented or equal, then it means that the current value is null if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { // Spec: Keys MUST be unique; first one wins. // But overwriting is allowed when a merge node is used in current block. if ($allowOverwrite || !isset($data[$key])) { $data[$key] = null; } } else { $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap); // Spec: Keys MUST be unique; first one wins. // But overwriting is allowed when a merge node is used in current block. if ($allowOverwrite || !isset($data[$key])) { $data[$key] = $value; } } } else { $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context); // Spec: Keys MUST be unique; first one wins. // But overwriting is allowed when a merge node is used in current block. if ($allowOverwrite || !isset($data[$key])) { $data[$key] = $value; } } if ($isRef) { $this->refs[$isRef] = $data[$key]; } } else { // multiple documents are not supported if ('---' === $this->currentLine) { throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine); } // 1-liner optionally followed by newline(s) if (is_string($value) && $this->lines[0] === trim($value)) { try { $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); throw $e; } if (isset($mbEncoding)) { mb_internal_encoding($mbEncoding); } return $value; } switch (preg_last_error()) { case PREG_INTERNAL_ERROR: $error = 'Internal PCRE error.'; break; case PREG_BACKTRACK_LIMIT_ERROR: $error = 'pcre.backtrack_limit reached.'; break; case PREG_RECURSION_LIMIT_ERROR: $error = 'pcre.recursion_limit reached.'; break; case PREG_BAD_UTF8_ERROR: $error = 'Malformed UTF-8 data.'; break; case PREG_BAD_UTF8_OFFSET_ERROR: $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.'; break; default: $error = 'Unable to parse.'; } throw new ParseException($error, $this->getRealCurrentLineNb() + 1, $this->currentLine); } } if (isset($mbEncoding)) { mb_internal_encoding($mbEncoding); } if ($objectForMap && !is_object($data) && 'mapping' === $context) { $object = new \stdClass(); foreach ($data as $key => $value) { $object->$key = $value; } $data = $object; } return empty($data) ? null : $data; } private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap) { $skippedLineNumbers = $this->skippedLineNumbers; foreach ($this->locallySkippedLineNumbers as $lineNumber) { if ($lineNumber < $offset) { continue; } $skippedLineNumbers[] = $lineNumber; } $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers); $parser->refs = &$this->refs; return $parser->parse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap); } /** * Returns the current line number (takes the offset into account). * * @return int The current line number */ private function getRealCurrentLineNb() { $realCurrentLineNumber = $this->currentLineNb + $this->offset; foreach ($this->skippedLineNumbers as $skippedLineNumber) { if ($skippedLineNumber > $realCurrentLineNumber) { break; } ++$realCurrentLineNumber; } return $realCurrentLineNumber; } /** * Returns the current line indentation. * * @return int The current line indentation */ private function getCurrentLineIndentation() { return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' ')); } /** * Returns the next embed block of YAML. * * @param int $indentation The indent level at which the block is to be read, or null for default * @param bool $inSequence True if the enclosing data structure is a sequence * * @return string A YAML string * * @throws ParseException When indentation problem are detected */ private function getNextEmbedBlock($indentation = null, $inSequence = false) { $oldLineIndentation = $this->getCurrentLineIndentation(); $blockScalarIndentations = array(); if ($this->isBlockScalarHeader()) { $blockScalarIndentations[] = $this->getCurrentLineIndentation(); } if (!$this->moveToNextLine()) { return; } if (null === $indentation) { $newIndent = $this->getCurrentLineIndentation(); $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem(); if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) { throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } } else { $newIndent = $indentation; } $data = array(); if ($this->getCurrentLineIndentation() >= $newIndent) { $data[] = substr($this->currentLine, $newIndent); } else { $this->moveToPreviousLine(); return; } if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) { // the previous line contained a dash but no item content, this line is a sequence item with the same indentation // and therefore no nested list or mapping $this->moveToPreviousLine(); return; } $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem(); if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) { $blockScalarIndentations[] = $this->getCurrentLineIndentation(); } $previousLineIndentation = $this->getCurrentLineIndentation(); while ($this->moveToNextLine()) { $indent = $this->getCurrentLineIndentation(); // terminate all block scalars that are more indented than the current line if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') { foreach ($blockScalarIndentations as $key => $blockScalarIndentation) { if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) { unset($blockScalarIndentations[$key]); } } } if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) { $blockScalarIndentations[] = $this->getCurrentLineIndentation(); } $previousLineIndentation = $indent; if ($isItUnindentedCollection && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) { $this->moveToPreviousLine(); break; } if ($this->isCurrentLineBlank()) { $data[] = substr($this->currentLine, $newIndent); continue; } // we ignore "comment" lines only when we are not inside a scalar block if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) { // remember ignored comment lines (they are used later in nested // parser calls to determine real line numbers) // // CAUTION: beware to not populate the global property here as it // will otherwise influence the getRealCurrentLineNb() call here // for consecutive comment lines and subsequent embedded blocks $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb(); continue; } if ($indent >= $newIndent) { $data[] = substr($this->currentLine, $newIndent); } elseif (0 == $indent) { $this->moveToPreviousLine(); break; } else { throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } } return implode("\n", $data); } /** * Moves the parser to the next line. * * @return bool */ private function moveToNextLine() { if ($this->currentLineNb >= count($this->lines) - 1) { return false; } $this->currentLine = $this->lines[++$this->currentLineNb]; return true; } /** * Moves the parser to the previous line. * * @return bool */ private function moveToPreviousLine() { if ($this->currentLineNb < 1) { return false; } $this->currentLine = $this->lines[--$this->currentLineNb]; return true; } /** * Parses a YAML value. * * @param string $value A YAML value * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise * @param bool $objectSupport True if object support is enabled, false otherwise * @param bool $objectForMap true if maps should return a stdClass instead of array() * @param string $context The parser context (either sequence or mapping) * * @return mixed A PHP value * * @throws ParseException When reference does not exist */ private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context) { if (0 === strpos($value, '*')) { if (false !== $pos = strpos($value, '#')) { $value = substr($value, 1, $pos - 2); } else { $value = substr($value, 1); } if (!array_key_exists($value, $this->refs)) { throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine); } return $this->refs[$value]; } if (preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) { $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : ''; return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers)); } try { $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs); if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) { @trigger_error(sprintf('Using a colon in the unquoted mapping value "%s" in line %d is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $value, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED); // to be thrown in 3.0 // throw new ParseException('A colon cannot be used in an unquoted mapping value.'); } return $parsedValue; } catch (ParseException $e) { $e->setParsedLine($this->getRealCurrentLineNb() + 1); $e->setSnippet($this->currentLine); throw $e; } } /** * Parses a block scalar. * * @param string $style The style indicator that was used to begin this block scalar (| or >) * @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -) * @param int $indentation The indentation indicator that was used to begin this block scalar * * @return string The text value */ private function parseBlockScalar($style, $chomping = '', $indentation = 0) { $notEOF = $this->moveToNextLine(); if (!$notEOF) { return ''; } $isCurrentLineBlank = $this->isCurrentLineBlank(); $blockLines = array(); // leading blank lines are consumed before determining indentation while ($notEOF && $isCurrentLineBlank) { // newline only if not EOF if ($notEOF = $this->moveToNextLine()) { $blockLines[] = ''; $isCurrentLineBlank = $this->isCurrentLineBlank(); } } // determine indentation if not specified if (0 === $indentation) { if (preg_match('/^ +/', $this->currentLine, $matches)) { $indentation = strlen($matches[0]); } } if ($indentation > 0) { $pattern = sprintf('/^ {%d}(.*)$/', $indentation); while ( $notEOF && ( $isCurrentLineBlank || preg_match($pattern, $this->currentLine, $matches) ) ) { if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) { $blockLines[] = substr($this->currentLine, $indentation); } elseif ($isCurrentLineBlank) { $blockLines[] = ''; } else { $blockLines[] = $matches[1]; } // newline only if not EOF if ($notEOF = $this->moveToNextLine()) { $isCurrentLineBlank = $this->isCurrentLineBlank(); } } } elseif ($notEOF) { $blockLines[] = ''; } if ($notEOF) { $blockLines[] = ''; $this->moveToPreviousLine(); } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) { $blockLines[] = ''; } // folded style if ('>' === $style) { $text = ''; $previousLineIndented = false; $previousLineBlank = false; for ($i = 0; $i < count($blockLines); ++$i) { if ('' === $blockLines[$i]) { $text .= "\n"; $previousLineIndented = false; $previousLineBlank = true; } elseif (' ' === $blockLines[$i][0]) { $text .= "\n".$blockLines[$i]; $previousLineIndented = true; $previousLineBlank = false; } elseif ($previousLineIndented) { $text .= "\n".$blockLines[$i]; $previousLineIndented = false; $previousLineBlank = false; } elseif ($previousLineBlank || 0 === $i) { $text .= $blockLines[$i]; $previousLineIndented = false; $previousLineBlank = false; } else { $text .= ' '.$blockLines[$i]; $previousLineIndented = false; $previousLineBlank = false; } } } else { $text = implode("\n", $blockLines); } // deal with trailing newlines if ('' === $chomping) { $text = preg_replace('/\n+$/', "\n", $text); } elseif ('-' === $chomping) { $text = preg_replace('/\n+$/', '', $text); } return $text; } /** * Returns true if the next line is indented. * * @return bool Returns true if the next line is indented, false otherwise */ private function isNextLineIndented() { $currentIndentation = $this->getCurrentLineIndentation(); $EOF = !$this->moveToNextLine(); while (!$EOF && $this->isCurrentLineEmpty()) { $EOF = !$this->moveToNextLine(); } if ($EOF) { return false; } $ret = false; if ($this->getCurrentLineIndentation() > $currentIndentation) { $ret = true; } $this->moveToPreviousLine(); return $ret; } /** * Returns true if the current line is blank or if it is a comment line. * * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise */ private function isCurrentLineEmpty() { return $this->isCurrentLineBlank() || $this->isCurrentLineComment(); } /** * Returns true if the current line is blank. * * @return bool Returns true if the current line is blank, false otherwise */ private function isCurrentLineBlank() { return '' == trim($this->currentLine, ' '); } /** * Returns true if the current line is a comment line. * * @return bool Returns true if the current line is a comment line, false otherwise */ private function isCurrentLineComment() { //checking explicitly the first char of the trim is faster than loops or strpos $ltrimmedLine = ltrim($this->currentLine, ' '); return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#'; } private function isCurrentLineLastLineInDocument() { return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1); } /** * Cleanups a YAML string to be parsed. * * @param string $value The input YAML string * * @return string A cleaned up YAML string */ private function cleanup($value) { $value = str_replace(array("\r\n", "\r"), "\n", $value); // strip YAML header $count = 0; $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count); $this->offset += $count; // remove leading comments $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count); if ($count == 1) { // items have been removed, update the offset $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); $value = $trimmedValue; } // remove start of the document marker (---) $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count); if ($count == 1) { // items have been removed, update the offset $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n"); $value = $trimmedValue; // remove end of the document marker (...) $value = preg_replace('#\.\.\.\s*$#', '', $value); } return $value; } /** * Returns true if the next line starts unindented collection. * * @return bool Returns true if the next line starts unindented collection, false otherwise */ private function isNextLineUnIndentedCollection() { $currentIndentation = $this->getCurrentLineIndentation(); $notEOF = $this->moveToNextLine(); while ($notEOF && $this->isCurrentLineEmpty()) { $notEOF = $this->moveToNextLine(); } if (false === $notEOF) { return false; } $ret = false; if ( $this->getCurrentLineIndentation() == $currentIndentation && $this->isStringUnIndentedCollectionItem() ) { $ret = true; } $this->moveToPreviousLine(); return $ret; } /** * Returns true if the string is un-indented collection item. * * @return bool Returns true if the string is un-indented collection item, false otherwise */ private function isStringUnIndentedCollectionItem() { return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- '); } /** * Tests whether or not the current line is the header of a block scalar. * * @return bool */ private function isBlockScalarHeader() { return (bool) preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; /** * Unescaper encapsulates unescaping rules for single and double-quoted * YAML strings. * * @author Matthew Lewinski * * @internal */ class Unescaper { /** * Parser and Inline assume UTF-8 encoding, so escaped Unicode characters * must be converted to that encoding. * * @deprecated since version 2.5, to be removed in 3.0 * * @internal */ const ENCODING = 'UTF-8'; /** * Regex fragment that matches an escaped character in a double quoted string. */ const REGEX_ESCAPED_CHARACTER = '\\\\(x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|.)'; /** * Unescapes a single quoted string. * * @param string $value A single quoted string * * @return string The unescaped string */ public function unescapeSingleQuotedString($value) { return str_replace('\'\'', '\'', $value); } /** * Unescapes a double quoted string. * * @param string $value A double quoted string * * @return string The unescaped string */ public function unescapeDoubleQuotedString($value) { $self = $this; $callback = function ($match) use ($self) { return $self->unescapeCharacter($match[0]); }; // evaluate the string return preg_replace_callback('/'.self::REGEX_ESCAPED_CHARACTER.'/u', $callback, $value); } /** * Unescapes a character that was found in a double-quoted string. * * @param string $value An escaped character * * @return string The unescaped character * * @internal This method is public to be usable as callback. It should not * be used in user code. Should be changed in 3.0. */ public function unescapeCharacter($value) { switch ($value[1]) { case '0': return "\x0"; case 'a': return "\x7"; case 'b': return "\x8"; case 't': return "\t"; case "\t": return "\t"; case 'n': return "\n"; case 'v': return "\xB"; case 'f': return "\xC"; case 'r': return "\r"; case 'e': return "\x1B"; case ' ': return ' '; case '"': return '"'; case '/': return '/'; case '\\': return '\\'; case 'N': // U+0085 NEXT LINE return "\xC2\x85"; case '_': // U+00A0 NO-BREAK SPACE return "\xC2\xA0"; case 'L': // U+2028 LINE SEPARATOR return "\xE2\x80\xA8"; case 'P': // U+2029 PARAGRAPH SEPARATOR return "\xE2\x80\xA9"; case 'x': return self::utf8chr(hexdec(substr($value, 2, 2))); case 'u': return self::utf8chr(hexdec(substr($value, 2, 4))); case 'U': return self::utf8chr(hexdec(substr($value, 2, 8))); default: @trigger_error('Not escaping a backslash in a double-quoted string is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', E_USER_DEPRECATED); return $value; } } /** * Get the UTF-8 character for the given code point. * * @param int $c The unicode code point * * @return string The corresponding UTF-8 character */ private static function utf8chr($c) { if (0x80 > $c %= 0x200000) { return chr($c); } if (0x800 > $c) { return chr(0xC0 | $c >> 6).chr(0x80 | $c & 0x3F); } if (0x10000 > $c) { return chr(0xE0 | $c >> 12).chr(0x80 | $c >> 6 & 0x3F).chr(0x80 | $c & 0x3F); } return chr(0xF0 | $c >> 18).chr(0x80 | $c >> 12 & 0x3F).chr(0x80 | $c >> 6 & 0x3F).chr(0x80 | $c & 0x3F); } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Component\Yaml; use Symfony\Component\Yaml\Exception\ParseException; /** * Yaml offers convenience methods to load and dump YAML. * * @author Fabien Potencier */ class Yaml { /** * Parses YAML into a PHP value. * * Usage: * * $array = Yaml::parse(file_get_contents('config.yml')); * print_r($array); * * * As this method accepts both plain strings and file names as an input, * you must validate the input before calling this method. Passing a file * as an input is a deprecated feature and will be removed in 3.0. * * Note: the ability to pass file names to the Yaml::parse method is deprecated since version 2.2 and will be removed in 3.0. Pass the YAML contents of the file instead. * * @param string $input Path to a YAML file or a string containing YAML * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise * @param bool $objectSupport True if object support is enabled, false otherwise * @param bool $objectForMap True if maps should return a stdClass instead of array() * * @return mixed The YAML converted to a PHP value * * @throws ParseException If the YAML is not valid */ public static function parse($input, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false) { // if input is a file, process it $file = ''; if (strpos($input, "\n") === false && is_file($input)) { @trigger_error('The ability to pass file names to the '.__METHOD__.' method is deprecated since version 2.2 and will be removed in 3.0. Pass the YAML contents of the file instead.', E_USER_DEPRECATED); if (false === is_readable($input)) { throw new ParseException(sprintf('Unable to parse "%s" as the file is not readable.', $input)); } $file = $input; $input = file_get_contents($file); } $yaml = new Parser(); try { return $yaml->parse($input, $exceptionOnInvalidType, $objectSupport, $objectForMap); } catch (ParseException $e) { if ($file) { $e->setParsedFile($file); } throw $e; } } /** * Dumps a PHP value to a YAML string. * * The dump method, when supplied with an array, will do its best * to convert the array into friendly YAML. * * @param mixed $input The PHP value * @param int $inline The level where you switch to inline YAML * @param int $indent The amount of spaces to use for indentation of nested nodes * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise * @param bool $objectSupport true if object support is enabled, false otherwise * * @return string A YAML string representing the original PHP value */ public static function dump($input, $inline = 2, $indent = 4, $exceptionOnInvalidType = false, $objectSupport = false) { if ($indent < 1) { throw new \InvalidArgumentException('The indentation must be greater than zero.'); } $yaml = new Dumper(); $yaml->setIndentation($indent); return $yaml->dump($input, $inline, 0, $exceptionOnInvalidType, $objectSupport); } } abstract; } else { $this->abstract = (bool)$toggle; } } /** * @param null|bool $toggle * @return bool */ public function hasIgnoreTag($toggle=null) { if( $toggle === null ) { return $this->hasIgnoreTag; } else { $this->hasIgnoreTag = (bool)$toggle; } } /** * @param null|bool $toggle * @return bool */ public function isInterface($toggle=null) { if( $toggle === null ) { return $this->isInterface; } else { $this->isInterface = (bool)$toggle; } } /** * @param string $extends */ public function setExtends($extends) { $this->extends = Utils::sanitizeClassName($extends); } /** * @return string */ public function getExtends() { return $this->extends; } /** * @param \PHPDocsMD\FunctionEntity[] $functions */ public function setFunctions(array $functions) { $this->functions = $functions; } /** * @param array $implements */ public function setInterfaces(array $implements) { $this->interfaces = array(); foreach($implements as $interface) { $this->interfaces[] = Utils::sanitizeClassName($interface); } } /** * @return array */ public function getInterfaces() { return $this->interfaces; } /** * @return \PHPDocsMD\FunctionEntity[] */ public function getFunctions() { return $this->functions; } /** * @param string $name */ function setName($name) { parent::setName(Utils::sanitizeClassName($name)); } /** * Check whether this object is referring to given class name or object instance * @param string|object $class * @return bool */ function isSame($class) { $className = is_object($class) ? get_class($class) : $class; return Utils::sanitizeClassName($className) == $this->getName(); } /** * Generate a title describing the class this object is referring to * @param string $format * @return string */ function generateTitle($format='%label%: %name% %extra%') { $translate = array( '%label%' => $this->isInterface() ? 'Interface' : 'Class', '%name%' => substr_count($this->getName(), '\\') == 1 ? substr($this->getName(), 1) : $this->getName(), '%extra%' => '' ); if( strpos($format, '%label%') === false ) { if( $this->isInterface() ) $translate['%extra%'] = '(interface)'; elseif( $this->isAbstract() ) $translate['%extra%'] = '(abstract)'; } else { $translate['%extra%'] = $this->isAbstract() && !$this->isInterface() ? '(abstract)' : ''; } return trim(strtr($format, $translate)); } /** * Generates an anchor link out of the generated title (see generateTitle) * @return string */ function generateAnchor() { $title = $this->generateTitle(); return strtolower(str_replace(array(':', ' ', '\\', '(', ')'), array('', '-', '', '', ''), $title)); } } docInfoExtractor = $docInfoExtractor; } public function create(\ReflectionClass $reflection) { $class = new ClassEntity(); $docInfo = $this->docInfoExtractor->extractInfo($reflection); $this->docInfoExtractor->applyInfoToEntity($reflection, $docInfo, $class); $class->isInterface($reflection->isInterface()); $class->isAbstract($reflection->isAbstract()); $class->setInterfaces(array_keys($reflection->getInterfaces())); $class->hasIgnoreTag($docInfo->shouldBeIgnored()); if ($reflection->getParentClass()) { $class->setExtends($reflection->getParentClass()->getName()); } return $class; } }isDeprecated; } else { $this->isDeprecated = (bool)$toggle; } } /** * @param string $description */ public function setDescription($description) { $this->description = $description; } /** * @return string */ public function getDescription() { return $this->description; } /** * @param string $name */ public function setName($name) { $this->name = $name; } /** * @return string */ public function getName() { return $this->name; } /** * @param string $deprecationMessage */ public function setDeprecationMessage($deprecationMessage) { $this->deprecationMessage = $deprecationMessage; } /** * @return string */ public function getDeprecationMessage() { return $this->deprecationMessage; } /** * @param string $example */ public function setExample($example) { $this->example = $example; } /** * @return string */ public function getExample() { return $this->example; } }version); } /** * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Input\OutputInterface $output * @return int */ public function run(InputInterface $input=null, OutputInterface $output=null) { $this->add(new PHPDocsMDCommand()); return parent::run($input, $output); } /** * Moves a copy of phpdoc-md to project root * @ignore */ public static function install() { $phpdocExec = __DIR__.'/../../../bin/phpdoc-md'; $projExec = getcwd().'/phpdoc-md'; copy($phpdocExec, $projExec); chmod($projExec, 0755); } }memory[$name]) ) { $reflector = new Reflector($name); $this->memory[$name] = $reflector->getClassEntity(); } return $this->memory[$name]; } protected function configure() { $this ->setName('generate') ->setDescription('Get docs for given class/source directory)') ->addArgument( self::ARG_CLASS, InputArgument::REQUIRED, 'Class or source directory' ) ->addOption( self::OPT_BOOTSTRAP, 'b', InputOption::VALUE_REQUIRED, 'File to be included before generating documentation' ) ->addOption( self::OPT_IGNORE, 'i', InputOption::VALUE_REQUIRED, 'Directories to ignore', '' ); } /** * @param InputInterface $input * @param OutputInterface $output * @return int|null|void * @throws \InvalidArgumentException */ protected function execute(InputInterface $input, OutputInterface $output) { $classes = $input->getArgument(self::ARG_CLASS); $bootstrap = $input->getOption(self::OPT_BOOTSTRAP); $ignore = explode(',', $input->getOption(self::OPT_IGNORE)); $requestingOneClass = false; if( $bootstrap ) { require_once strpos($bootstrap,'/') === 0 ? $bootstrap : getcwd().'/'.$bootstrap; } $classCollection = array(); if( strpos($classes, ',') !== false ) { foreach(explode(',', $classes) as $class) { if( class_exists($class) || interface_exists($class) ) $classCollection[0][] = $class; } } elseif( class_exists($classes) || interface_exists($classes) ) { $classCollection[] = array($classes); $requestingOneClass = true; } elseif( is_dir($classes) ) { $classCollection = $this->findClassesInDir($classes, array(), $ignore); } else { throw new \InvalidArgumentException('Given input is neither a class nor a source directory'); } $tableGenerator = new MDTableGenerator(); $tableOfContent = array(); $body = array(); $classLinks = array(); foreach($classCollection as $ns => $classes) { foreach($classes as $className) { $class = $this->getClassEntity($className); if( $class->hasIgnoreTag() ) continue; // Add to tbl of contents $tableOfContent[] = sprintf('- [%s](#%s)', $class->generateTitle('%name% %extra%'), $class->generateAnchor()); $classLinks[$class->generateAnchor()] = $class->getName(); // generate function table $tableGenerator->openTable(); $tableGenerator->doDeclareAbstraction(!$class->isInterface()); foreach($class->getFunctions() as $func) { $tableGenerator->addFunc($func); } $docs = ($requestingOneClass ? '':'
    ').PHP_EOL; if( $class->isDeprecated() ) { $docs .= '### '.$class->generateTitle().''.PHP_EOL.PHP_EOL. '> **DEPRECATED** '.$class->getDeprecationMessage().PHP_EOL.PHP_EOL; } else { $docs .= '### '.$class->generateTitle().PHP_EOL.PHP_EOL; if( $class->getDescription() ) $docs .= '> '.$class->getDescription().PHP_EOL.PHP_EOL; } if( $example = $class->getExample() ) { $docs .= '###### Example' . PHP_EOL . MDTableGenerator::formatExampleComment($example) .PHP_EOL.PHP_EOL; } $docs .= $tableGenerator->getTable().PHP_EOL; if( $class->getExtends() ) { $link = $class->getExtends(); if( $anchor = $this->getAnchorFromClassCollection($classCollection, $class->getExtends()) ) { $link = sprintf('[%s](#%s)', $link, $anchor); } $docs .= PHP_EOL.'*This class extends '.$link.'*'.PHP_EOL; } if( $interfaces = $class->getInterfaces() ) { $interfaceNames = array(); foreach($interfaces as $interface) { $anchor = $this->getAnchorFromClassCollection($classCollection, $interface); $interfaceNames[] = $anchor ? sprintf('[%s](#%s)', $interface, $anchor) : $interface; } $docs .= PHP_EOL.'*This class implements '.implode(', ', $interfaceNames).'*'.PHP_EOL; } $body[] = $docs; } } if( empty($tableOfContent) ) { throw new \InvalidArgumentException('No classes found'); } elseif( !$requestingOneClass ) { $output->writeln('## Table of contents'.PHP_EOL); $output->writeln(implode(PHP_EOL, $tableOfContent)); } // Convert references to classes into links asort($classLinks); $classLinks = array_reverse($classLinks, true); $docString = implode(PHP_EOL, $body); foreach($classLinks as $anchor => $className) { $link = sprintf('[%s](#%s)', $className, $anchor); $find = array(''.$className, '/'.$className); $replace = array(''.$link, '/'.$link); $docString = str_replace($find, $replace, $docString); } $output->writeln(PHP_EOL.$docString); } /** * @param $coll * @param $find * @return bool|string */ private function getAnchorFromClassCollection($coll, $find) { foreach($coll as $ns => $classes) { foreach($classes as $className) { if( $className == $find ) { return $this->getClassEntity($className)->generateAnchor(); } } } return false; } /** * @param $file * @return array */ private function findClassInFile($file) { $ns = ''; $class = false; foreach(explode(PHP_EOL, file_get_contents($file)) as $line) { if ( strpos($line, '*') === false ) { if( strpos($line, 'namespace') !== false ) { $ns = trim(current(array_slice(explode('namespace', $line), 1)), '; '); $ns = Utils::sanitizeClassName($ns); } elseif( strpos($line, 'class') !== false ) { $class = $this->extractClassNameFromLine('class', $line); break; } elseif( strpos($line, 'interface') !== false ) { $class = $this->extractClassNameFromLine('interface', $line); break; } } } return $class ? array($ns, $ns .'\\'. $class) : array(false, false); } /** * @param string $type * @param string $line * @return string */ function extractClassNameFromLine($type, $line) { $class = trim(current(array_slice(explode($type, $line), 1)), '; '); return trim(current(explode(' ', $class))); } /** * @param $dir * @param array $collection * @param array $ignores * @return array */ private function findClassesInDir($dir, $collection=array(), $ignores=array()) { foreach(new \FilesystemIterator($dir) as $f) { /** @var \SplFileInfo $f */ if( $f->isFile() && !$f->isLink() ) { list($ns, $className) = $this->findClassInFile($f->getRealPath()); if( $className && (class_exists($className, true) || interface_exists($className)) ) { $collection[$ns][] = $className; } } elseif( $f->isDir() && !$f->isLink() && !$this->shouldIgnoreDirectory($f->getFilename(), $ignores) ) { $collection = $this->findClassesInDir($f->getRealPath(), $collection); } } ksort($collection); return $collection; } /** * @param $dirName * @param $ignores * @return bool */ private function shouldIgnoreDirectory($dirName, $ignores) { foreach($ignores as $dir) { $dir = trim($dir); if( !empty($dir) && substr($dirName, -1 * strlen($dir)) == $dir ) { return true; } } return false; } }data = array_merge( array( 'return' => '', 'params' => array(), 'description' => '', 'example' => false, 'deprecated' => false ), $data); } /** * @return string */ public function getReturnType() { return $this->data['return']; } /** * @return array */ public function getParameters() { return $this->data['params']; } /** * @param string $name * @return array */ public function getParameterInfo($name) { if (isset($this->data['params'][$name])) { return $this->data['params'][$name]; } return array(); } /** * @return string */ public function getExample() { return $this->data['example']; } /** * @return string */ public function getDescription() { return $this->data['description']; } /** * @return string */ public function getDeprecationMessage() { return $this->data['deprecated']; } /** * @return bool */ public function shouldInheritDoc() { return isset($this->data['inheritDoc']) || isset($this->data['inheritdoc']); } /** * @return bool */ public function shouldBeIgnored() { return isset($this->data['ignore']); } }getCleanDocComment($reflection); $data = $this->extractInfoFromComment($comment, $reflection); return new DocInfo($data); } /** * @param \ReflectionClass|\ReflectionMethod $reflection * @param DocInfo $docInfo * @param CodeEntity $code */ public function applyInfoToEntity($reflection, DocInfo $docInfo, CodeEntity $code) { $code->setName($reflection->getName()); $code->setDescription($docInfo->getDescription()); $code->setExample($docInfo->getExample()); if ($docInfo->getDeprecationMessage()) { $code->isDeprecated(true); $code->setDeprecationMessage($docInfo->getDeprecationMessage()); } } /** * @param \ReflectionClass $reflection * @return string */ private function getCleanDocComment($reflection) { $comment = str_replace(array('/*', '*/'), '', $reflection->getDocComment()); return trim(trim(preg_replace('/([\s|^]\*\s)/', '', $comment)), '*'); } /** * @param string $comment * @param string $current_tag * @param \ReflectionMethod|\ReflectionClass $reflection * @return array */ private function extractInfoFromComment($comment, $reflection, $current_tag='description') { $currentNamespace = $this->getNameSpace($reflection); $tags = array($current_tag=>''); foreach(explode(PHP_EOL, $comment) as $line) { if( $current_tag != 'example' ) $line = trim($line); $words = $this->getWordsFromLine($line); if( empty($words) ) continue; if( strpos($words[0], '@') === false ) { // Append to tag $joinWith = $current_tag == 'example' ? PHP_EOL : ' '; $tags[$current_tag] .= $joinWith . $line; } elseif( $words[0] == '@param' ) { // Get parameter declaration if( $paramData = $this->figureOutParamDeclaration($words, $currentNamespace) ) { list($name, $data) = $paramData; $tags['params'][$name] = $data; } } else { // Start new tag $current_tag = substr($words[0], 1); array_splice($words, 0 ,1); if( empty($tags[$current_tag]) ) { $tags[$current_tag] = ''; } $tags[$current_tag] .= trim(join(' ', $words)); } } foreach($tags as $name => $val) { if( is_array($val) ) { foreach($val as $subName=>$subVal) { if( is_string($subVal) ) $tags[$name][$subName] = trim($subVal); } } else { $tags[$name] = trim($val); } } return $tags; } /** * @param \ReflectionClass|\ReflectionMethod $reflection * @return string */ private function getNameSpace($reflection) { if ($reflection instanceof \ReflectionClass) { return $reflection->getNamespaceName(); } else { return $reflection->getDeclaringClass()->getNamespaceName(); } } /** * @param $line * @return array */ private function getWordsFromLine($line) { $words = array(); foreach(explode(' ', trim($line)) as $w) { if( !empty($w) ) { $words[] = $w; } } return $words; } /** * @param $words * @param $currentNameSpace * @return array|bool */ private function figureOutParamDeclaration($words, $currentNameSpace) { $description = ''; $type = ''; $name = ''; if (strpos($words[1], '$') === 0) { $name = $words[1]; $type = 'mixed'; array_splice($words, 0, 2); } elseif (isset($words[2])) { $name = $words[2]; $type = $words[1]; array_splice($words, 0, 3); } if (!empty($name)) { $name = current(explode('=', $name)); if( count($words) > 1 ) { $description = join(' ', $words); } $type = Utils::sanitizeDeclaration($type, $currentNameSpace); $data = array( 'description' => $description, 'name' => $name, 'type' => $type, 'default' => false ); return array($name, $data); } return false; } } isStatic; } else { $this->isStatic = (bool)$toggle; } } /** * @param null|bool $toggle */ public function isAbstract($toggle=null) { if ( $toggle === null ) { return $this->abstract; } else { $this->abstract = (bool)$toggle; } } /** * @return bool */ public function hasParams() { return !empty($this->params); } /** * @param \PHPDocsMD\ParamEntity[] $params */ public function setParams(array $params) { $this->params = $params; } /** * @return \PHPDocsMD\ParamEntity[] */ public function getParams() { return $this->params; } /** * @param string $returnType */ public function setReturnType($returnType) { $this->returnType = $returnType; } /** * @return string */ public function getReturnType() { return $this->returnType; } /** * @param string $visibility */ public function setVisibility($visibility) { $this->visibility = $visibility; } /** * @return string */ public function getVisibility() { return $this->visibility; } /** * @param string $class */ public function setClass($class) { $this->class = $class; } /** * @return string */ public function getClass() { return $this->class; } } find($methodName, $className); if (false !== $function) { return $function; } } return false; } /** * @param string $methodName * @param string $className * @return bool|FunctionEntity */ public function find($methodName, $className) { if ($className) { $classEntity = $this->loadClassEntity($className); $functions = $classEntity->getFunctions(); foreach($functions as $function) { if($function->getName() == $methodName) { return $function; } } if($classEntity->getExtends()) { return $this->find($methodName, $classEntity->getExtends()); } } return false; } /** * @param $className * @return ClassEntity */ private function loadClassEntity($className) { if (empty($this->cache[$className])) { $reflector = new Reflector($className, $this); $this->cache[$className] = $reflector->getClassEntity(); } return $this->cache[$className]; } } * openTable(); * foreach($classEntity->getFunctions() as $func) { * $generator->addFunc( $func ); * } * echo $generator->getTable(); * * * @package PHPDocsMD */ class MDTableGenerator { /** * @var string */ private $fullClassName = ''; /** * @var string */ private $markdown = ''; /** * @var array */ private $examples = array(); /** * @var bool */ private $appendExamples = true; /** * @var bool */ private $declareAbstraction = true; /** * @param $example * @return mixed */ private static function stripCodeTags($example) { if (strpos($example, '', $example), -2); $example = current($parts); $parts = array_slice(explode('', $example), 1); $example = current($parts); } return $example; } /** * All example comments found while generating the table will be * appended to the end of the table. Set $toggle to false to * prevent this behaviour * * @param bool $toggle */ function appendExamplesToEndOfTable($toggle) { $this->appendExamples = (bool)$toggle; } /** * Begin generating a new markdown-formatted table */ function openTable() { $this->examples = array(); $this->markdown = ''; // Clear table $this->declareAbstraction = true; $this->add('| Visibility | Function |'); $this->add('|:-----------|:---------|'); } /** * Toggle whether or not methods being abstract (or part of an interface) * should be declared as abstract in the table * @param bool $toggle */ function doDeclareAbstraction($toggle) { $this->declareAbstraction = (bool)$toggle; } /** * Generates a markdown formatted table row with information about given function. Then adds the * row to the table and returns the markdown formatted string. * * @param FunctionEntity $func * @return string */ function addFunc(FunctionEntity $func) { $this->fullClassName = $func->getClass(); $str = ''; if( $this->declareAbstraction && $func->isAbstract() ) $str .= 'abstract '; $str .= $func->getName().'('; if( $func->hasParams() ) { $params = array(); foreach($func->getParams() as $param) { $paramStr = ''.$param->getType().' '.$param->getName(); if( $param->getDefault() ) { $paramStr .= '='.$param->getDefault(); } $paramStr .= ''; $params[] = $paramStr; } $str .= ''.implode(', ', $params) .')'; } else { $str .= ')'; } $str .= ' : '.$func->getReturnType().''; if( $func->isDeprecated() ) { $str = ''.$str.''; $str .= '
    DEPRECATED - '.$func->getDeprecationMessage().''; } elseif( $func->getDescription() ) { $str .= '
    '.$func->getDescription().''; } $str = str_replace(array('', ' '), array('',''), trim($str)); if( $func->getExample() ) $this->examples[$func->getName()] = $func->getExample(); $firstCol = $func->getVisibility() . ($func->isStatic() ? ' static':''); $markDown = '| '.$firstCol.' | '.$str.' |'; $this->add($markDown); return $markDown; } /** * @return string */ function getTable() { $tbl = trim($this->markdown); if( $this->appendExamples && !empty($this->examples) ) { $className = Utils::getClassBaseName($this->fullClassName); foreach($this->examples as $funcName => $example) { $tbl .= sprintf("\n###### Examples of %s::%s()\n%s", $className, $funcName, self::formatExampleComment($example)); } } return $tbl; } /** * Create a markdown-formatted code view out of an example comment * @param string $example * @return string */ public static function formatExampleComment($example) { // Remove possible code tag $example = self::stripCodeTags($example); if( preg_match('/(\n )/', $example) ) { $example = preg_replace('/(\n )/', "\n", $example); } elseif( preg_match('/(\n )/', $example) ) { $example = preg_replace('/(\n )/', "\n", $example); } else { $example = preg_replace('/(\n )/', "\n", $example); } $type = ''; // A very naive analysis of the programming language used in the comment if( strpos($example, 'markdown .= $str .PHP_EOL; } }default = $default; } /** * @return boolean */ public function getDefault() { return $this->default; } /** * @param string $type */ public function setType($type) { $this->type = $type; } /** * @return string */ public function getType() { return $this->type; } } className = $className; $this->functionFinder = $this->loadIfNull($functionFinder, FunctionFinder::class); $this->docInfoExtractor = $this->loadIfNull($docInfoExtractor, DocInfoExtractor::class); $this->useInspector = $this->loadIfNull($useInspector, UseInspector::class); $this->classEntityFactory = $this->loadIfNull( $classEntityFactory, ClassEntityFactory::class, $this->docInfoExtractor ); } private function loadIfNull($obj, $className, $in=null) { return is_object($obj) ? $obj : new $className($in); } /** * @return \PHPDocsMD\ClassEntity */ function getClassEntity() { $classReflection = new \ReflectionClass($this->className); $classEntity = $this->classEntityFactory->create($classReflection); $classEntity->setFunctions($this->getClassFunctions($classEntity, $classReflection)); return $classEntity; } /** * @param ClassEntity $classEntity * @param \ReflectionClass $reflectionClass * @return FunctionEntity[] */ private function getClassFunctions(ClassEntity $classEntity, \ReflectionClass $reflectionClass) { $classUseStatements = $this->useInspector->getUseStatements($reflectionClass->getFileName()); $publicFunctions = array(); $protectedFunctions = array(); foreach($reflectionClass->getMethods() as $methodReflection) { $func = $this->createFunctionEntity( $methodReflection, $classEntity, $classUseStatements ); if( $func ) { if( $func->getVisibility() == 'public' ) { $publicFunctions[$func->getName()] = $func; } else { $protectedFunctions[$func->getName()] = $func; } } } ksort($publicFunctions); ksort($protectedFunctions); return array_values(array_merge($publicFunctions, $protectedFunctions)); } /** * @param ReflectionMethod $method * @param ClassEntity $class * @param array $useStatements * @return bool|FunctionEntity */ protected function createFunctionEntity(ReflectionMethod $method, ClassEntity $class, $useStatements) { $func = new FunctionEntity(); $docInfo = $this->docInfoExtractor->extractInfo($method); $this->docInfoExtractor->applyInfoToEntity($method, $docInfo, $func); if ($docInfo->shouldInheritDoc()) { return $this->findInheritedFunctionDeclaration($func, $class); } if ($this->shouldIgnoreFunction($docInfo, $method, $class)) { return false; } $func->setReturnType($this->getReturnType($docInfo, $method, $func, $useStatements)); $func->setParams($this->getParams($method, $docInfo)); $func->isStatic($method->isStatic()); $func->setVisibility($method->isPublic() ? 'public' : 'protected'); $func->isAbstract($method->isAbstract()); $func->setClass($class->getName()); return $func; } /** * @param DocInfo $docInfo * @param ReflectionMethod $method * @param FunctionEntity $func * @param array $useStatements * @return string */ private function getReturnType( DocInfo $docInfo, ReflectionMethod $method, FunctionEntity $func, array $useStatements ) { $returnType = $docInfo->getReturnType(); if (empty($returnType)) { $returnType = $this->guessReturnTypeFromFuncName($func->getName()); } elseif(Utils::isClassReference($returnType) && !class_exists($returnType)) { $className = $this->stripAwayNamespace($returnType); foreach ($useStatements as $usedClass) { if ($this->stripAwayNamespace($usedClass) == $className) { $returnType = $usedClass; break; } } } return Utils::sanitizeDeclaration( $returnType, $method->getDeclaringClass()->getNamespaceName() ); } /** * @param string $className * @return string */ private function stripAwayNamespace($className) { return trim(substr($className, strrpos($className, '\\')), '\\'); } /** * @param DocInfo $info * @param ReflectionMethod $method * @param ClassEntity $class * @return bool */ protected function shouldIgnoreFunction($info, ReflectionMethod $method, $class) { return $info->shouldBeIgnored() || $method->isPrivate() || !$class->isSame($method->getDeclaringClass()->getName()); } /** * @todo Turn this into a class "FunctionEntityFactory" * @param \ReflectionParameter $reflection * @param array $docs * @return FunctionEntity */ private function createParameterEntity(\ReflectionParameter $reflection, $docs) { // need to use slash instead of pipe or md-generation will get it wrong $def = false; $type = 'mixed'; $declaredType = self::getParamType($reflection); if( !isset($docs['type']) ) $docs['type'] = ''; if( $declaredType && !($declaredType=='array' && substr($docs['type'], -2) == '[]') && $declaredType != $docs['type']) { if( $declaredType && $docs['type'] ) { $posClassA = Utils::getClassBaseName($docs['type']); $posClassB = Utils::getClassBaseName($declaredType); if( $posClassA == $posClassB ) { $docs['type'] = $declaredType; } else { $docs['type'] = empty($docs['type']) ? $declaredType : $docs['type'].'/'.$declaredType; } } else { $docs['type'] = empty($docs['type']) ? $declaredType : $docs['type'].'/'.$declaredType; } } try { $def = $reflection->getDefaultValue(); $type = $this->getTypeFromVal($def); if( is_string($def) ) { $def = "`'$def'`"; } elseif( is_bool($def) ) { $def = $def ? 'true':'false'; } elseif( is_null($def) ) { $def = 'null'; } elseif( is_array($def) ) { $def = 'array()'; } } catch(\Exception $e) {} $varName = '$'.$reflection->getName(); if( !empty($docs) ) { $docs['default'] = $def; if( $type == 'mixed' && $def == 'null' && strpos($docs['type'], '\\') === 0 ) { $type = false; } if( $type && $def && !empty($docs['type']) && $docs['type'] != $type && strpos($docs['type'], '|') === false) { if( substr($docs['type'], strpos($docs['type'], '\\')) == substr($declaredType, strpos($declaredType, '\\')) ) { $docs['type'] = $declaredType; } else { $docs['type'] = $type.'/'.$docs['type']; } } elseif( $type && empty($docs['type']) ) { $docs['type'] = $type; } } else { $docs = array( 'descriptions'=>'', 'name' => $varName, 'default' => $def, 'type' => $type ); } $param = new ParamEntity(); $param->setDescription(isset($docs['description']) ? $docs['description']:''); $param->setName($varName); $param->setDefault($docs['default']); $param->setType(empty($docs['type']) ? 'mixed':str_replace(array('|', '\\\\'), array('/', '\\'), $docs['type'])); return $param; } /** * Tries to find out if the type of the given parameter. Will * return empty string if not possible. * * @example * * getMethods() as $method ) { * foreach($method->getParameters() as $param) { * $name = $param->getName(); * $type = Reflector::getParamType($param); * printf("%s = %s\n", $name, $type); * } * } * * * @param \ReflectionParameter $refParam * @return string */ static function getParamType(\ReflectionParameter $refParam) { $export = \ReflectionParameter::export( array( $refParam->getDeclaringClass()->name, $refParam->getDeclaringFunction()->name ), $refParam->name, true ); $export = str_replace(' or NULL', '', $export); $type = preg_replace('/.*?([\w\\\]+)\s+\$'.current(explode('=', $refParam->name)).'.*/', '\\1', $export); if( strpos($type, 'Parameter ') !== false ) { return ''; } if( $type != 'array' && strpos($type, '\\') !== 0 ) { $type = '\\'.$type; } return $type; } /** * @param string $name * @return string */ private function guessReturnTypeFromFuncName($name) { $mixed = array('get', 'load', 'fetch', 'find', 'create'); $bool = array('is', 'can', 'has', 'have', 'should'); foreach($mixed as $prefix) { if( strpos($name, $prefix) === 0 ) return 'mixed'; } foreach($bool as $prefix) { if( strpos($name, $prefix) === 0 ) return 'bool'; } return 'void'; } /** * @param string $def * @return string */ private function getTypeFromVal($def) { if( is_string($def) ) { return 'string'; } elseif( is_bool($def) ) { return 'bool'; } elseif( is_array($def) ) { return 'array'; } else { return 'mixed'; } } /** * @param FunctionEntity $func * @param ClassEntity $class * @return FunctionEntity */ private function findInheritedFunctionDeclaration(FunctionEntity $func, ClassEntity $class) { $funcName = $func->getName(); $inheritedFuncDeclaration = $this->functionFinder->find( $funcName, $class->getExtends() ); if (!$inheritedFuncDeclaration) { $inheritedFuncDeclaration = $this->functionFinder->findInClasses( $funcName, $class->getInterfaces() ); if (!$inheritedFuncDeclaration) { throw new \RuntimeException( 'Function '.$funcName.' tries to inherit docs but no parent method is found' ); } } return $inheritedFuncDeclaration; } /** * @param ReflectionMethod $method * @param DocInfo $docInfo * @return array */ private function getParams(ReflectionMethod $method, $docInfo) { $params = array(); foreach ($method->getParameters() as $param) { $paramName = '$' . $param->getName(); $params[$param->getName()] = $this->createParameterEntity( $param, $docInfo->getParameterInfo($paramName) ); } return array_values($params); } } getUseStatementsInString(file_get_contents($filePath)); } }$p) { if (self::shouldPrefixWithNamespace($p)) { $p = self::sanitizeClassName('\\' . trim($currentNameSpace, '\\') . '\\' . $p); } elseif (self::isClassReference($p)) { $p = self::sanitizeClassName($p); } $parts[$i] = $p; } return implode('/', $parts); } /** * @param string $typeDeclaration * @return bool */ private static function shouldPrefixWithNameSpace($typeDeclaration) { return strpos($typeDeclaration, '\\') !== 0 && self::isClassReference($typeDeclaration); } /** * @param string $typeDeclaration * @return bool */ public static function isClassReference($typeDeclaration) { $natives = array('mixed', 'string', 'int', 'float', 'integer', 'number', 'bool', 'boolean', 'object', 'false', 'true', 'null', 'array', 'void'); $sanitizedTypeDeclaration = rtrim(trim(strtolower($typeDeclaration)), '[]'); return !in_array($sanitizedTypeDeclaration, $natives) && strpos($typeDeclaration, ' ') === false; } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Webmozart\Assert; use BadMethodCallException; use InvalidArgumentException; use Traversable; use Exception; use Throwable; use Closure; /** * Efficient assertions to validate the input/output of your methods. * * @method static void nullOrString($value, $message = '') * @method static void nullOrStringNotEmpty($value, $message = '') * @method static void nullOrInteger($value, $message = '') * @method static void nullOrIntegerish($value, $message = '') * @method static void nullOrFloat($value, $message = '') * @method static void nullOrNumeric($value, $message = '') * @method static void nullOrBoolean($value, $message = '') * @method static void nullOrScalar($value, $message = '') * @method static void nullOrObject($value, $message = '') * @method static void nullOrResource($value, $type = null, $message = '') * @method static void nullOrIsCallable($value, $message = '') * @method static void nullOrIsArray($value, $message = '') * @method static void nullOrIsTraversable($value, $message = '') * @method static void nullOrIsInstanceOf($value, $class, $message = '') * @method static void nullOrNotInstanceOf($value, $class, $message = '') * @method static void nullOrIsEmpty($value, $message = '') * @method static void nullOrNotEmpty($value, $message = '') * @method static void nullOrTrue($value, $message = '') * @method static void nullOrFalse($value, $message = '') * @method static void nullOrEq($value, $value2, $message = '') * @method static void nullOrNotEq($value,$value2, $message = '') * @method static void nullOrSame($value, $value2, $message = '') * @method static void nullOrNotSame($value, $value2, $message = '') * @method static void nullOrGreaterThan($value, $value2, $message = '') * @method static void nullOrGreaterThanEq($value, $value2, $message = '') * @method static void nullOrLessThan($value, $value2, $message = '') * @method static void nullOrLessThanEq($value, $value2, $message = '') * @method static void nullOrRange($value, $min, $max, $message = '') * @method static void nullOrOneOf($value, $values, $message = '') * @method static void nullOrContains($value, $subString, $message = '') * @method static void nullOrStartsWith($value, $prefix, $message = '') * @method static void nullOrStartsWithLetter($value, $message = '') * @method static void nullOrEndsWith($value, $suffix, $message = '') * @method static void nullOrRegex($value, $pattern, $message = '') * @method static void nullOrAlpha($value, $message = '') * @method static void nullOrDigits($value, $message = '') * @method static void nullOrAlnum($value, $message = '') * @method static void nullOrLower($value, $message = '') * @method static void nullOrUpper($value, $message = '') * @method static void nullOrLength($value, $length, $message = '') * @method static void nullOrMinLength($value, $min, $message = '') * @method static void nullOrMaxLength($value, $max, $message = '') * @method static void nullOrLengthBetween($value, $min, $max, $message = '') * @method static void nullOrFileExists($value, $message = '') * @method static void nullOrFile($value, $message = '') * @method static void nullOrDirectory($value, $message = '') * @method static void nullOrReadable($value, $message = '') * @method static void nullOrWritable($value, $message = '') * @method static void nullOrClassExists($value, $message = '') * @method static void nullOrSubclassOf($value, $class, $message = '') * @method static void nullOrImplementsInterface($value, $interface, $message = '') * @method static void nullOrPropertyExists($value, $property, $message = '') * @method static void nullOrPropertyNotExists($value, $property, $message = '') * @method static void nullOrMethodExists($value, $method, $message = '') * @method static void nullOrMethodNotExists($value, $method, $message = '') * @method static void nullOrKeyExists($value, $key, $message = '') * @method static void nullOrKeyNotExists($value, $key, $message = '') * @method static void nullOrCount($value, $key, $message = '') * @method static void nullOrUuid($values, $message = '') * @method static void allString($values, $message = '') * @method static void allStringNotEmpty($values, $message = '') * @method static void allInteger($values, $message = '') * @method static void allIntegerish($values, $message = '') * @method static void allFloat($values, $message = '') * @method static void allNumeric($values, $message = '') * @method static void allBoolean($values, $message = '') * @method static void allScalar($values, $message = '') * @method static void allObject($values, $message = '') * @method static void allResource($values, $type = null, $message = '') * @method static void allIsCallable($values, $message = '') * @method static void allIsArray($values, $message = '') * @method static void allIsTraversable($values, $message = '') * @method static void allIsInstanceOf($values, $class, $message = '') * @method static void allNotInstanceOf($values, $class, $message = '') * @method static void allNull($values, $message = '') * @method static void allNotNull($values, $message = '') * @method static void allIsEmpty($values, $message = '') * @method static void allNotEmpty($values, $message = '') * @method static void allTrue($values, $message = '') * @method static void allFalse($values, $message = '') * @method static void allEq($values, $value2, $message = '') * @method static void allNotEq($values,$value2, $message = '') * @method static void allSame($values, $value2, $message = '') * @method static void allNotSame($values, $value2, $message = '') * @method static void allGreaterThan($values, $value2, $message = '') * @method static void allGreaterThanEq($values, $value2, $message = '') * @method static void allLessThan($values, $value2, $message = '') * @method static void allLessThanEq($values, $value2, $message = '') * @method static void allRange($values, $min, $max, $message = '') * @method static void allOneOf($values, $values, $message = '') * @method static void allContains($values, $subString, $message = '') * @method static void allStartsWith($values, $prefix, $message = '') * @method static void allStartsWithLetter($values, $message = '') * @method static void allEndsWith($values, $suffix, $message = '') * @method static void allRegex($values, $pattern, $message = '') * @method static void allAlpha($values, $message = '') * @method static void allDigits($values, $message = '') * @method static void allAlnum($values, $message = '') * @method static void allLower($values, $message = '') * @method static void allUpper($values, $message = '') * @method static void allLength($values, $length, $message = '') * @method static void allMinLength($values, $min, $message = '') * @method static void allMaxLength($values, $max, $message = '') * @method static void allLengthBetween($values, $min, $max, $message = '') * @method static void allFileExists($values, $message = '') * @method static void allFile($values, $message = '') * @method static void allDirectory($values, $message = '') * @method static void allReadable($values, $message = '') * @method static void allWritable($values, $message = '') * @method static void allClassExists($values, $message = '') * @method static void allSubclassOf($values, $class, $message = '') * @method static void allImplementsInterface($values, $interface, $message = '') * @method static void allPropertyExists($values, $property, $message = '') * @method static void allPropertyNotExists($values, $property, $message = '') * @method static void allMethodExists($values, $method, $message = '') * @method static void allMethodNotExists($values, $method, $message = '') * @method static void allKeyExists($values, $key, $message = '') * @method static void allKeyNotExists($values, $key, $message = '') * @method static void allCount($values, $key, $message = '') * @method static void allUuid($values, $message = '') * * @since 1.0 * * @author Bernhard Schussek */ class Assert { public static function string($value, $message = '') { if (!is_string($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a string. Got: %s', static::typeToString($value) )); } } public static function stringNotEmpty($value, $message = '') { static::string($value, $message); static::notEmpty($value, $message); } public static function integer($value, $message = '') { if (!is_int($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected an integer. Got: %s', static::typeToString($value) )); } } public static function integerish($value, $message = '') { if (!is_numeric($value) || $value != (int) $value) { static::reportInvalidArgument(sprintf( $message ?: 'Expected an integerish value. Got: %s', static::typeToString($value) )); } } public static function float($value, $message = '') { if (!is_float($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a float. Got: %s', static::typeToString($value) )); } } public static function numeric($value, $message = '') { if (!is_numeric($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a numeric. Got: %s', static::typeToString($value) )); } } public static function boolean($value, $message = '') { if (!is_bool($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a boolean. Got: %s', static::typeToString($value) )); } } public static function scalar($value, $message = '') { if (!is_scalar($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a scalar. Got: %s', static::typeToString($value) )); } } public static function object($value, $message = '') { if (!is_object($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected an object. Got: %s', static::typeToString($value) )); } } public static function resource($value, $type = null, $message = '') { if (!is_resource($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a resource. Got: %s', static::typeToString($value) )); } if ($type && $type !== get_resource_type($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a resource of type %2$s. Got: %s', static::typeToString($value), $type )); } } public static function isCallable($value, $message = '') { if (!is_callable($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a callable. Got: %s', static::typeToString($value) )); } } public static function isArray($value, $message = '') { if (!is_array($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected an array. Got: %s', static::typeToString($value) )); } } public static function isTraversable($value, $message = '') { if (!is_array($value) && !($value instanceof Traversable)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a traversable. Got: %s', static::typeToString($value) )); } } public static function isInstanceOf($value, $class, $message = '') { if (!($value instanceof $class)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected an instance of %2$s. Got: %s', static::typeToString($value), $class )); } } public static function notInstanceOf($value, $class, $message = '') { if ($value instanceof $class) { static::reportInvalidArgument(sprintf( $message ?: 'Expected an instance other than %2$s. Got: %s', static::typeToString($value), $class )); } } public static function isEmpty($value, $message = '') { if (!empty($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected an empty value. Got: %s', static::valueToString($value) )); } } public static function notEmpty($value, $message = '') { if (empty($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a non-empty value. Got: %s', static::valueToString($value) )); } } public static function null($value, $message = '') { if (null !== $value) { static::reportInvalidArgument(sprintf( $message ?: 'Expected null. Got: %s', static::valueToString($value) )); } } public static function notNull($value, $message = '') { if (null === $value) { static::reportInvalidArgument( $message ?: 'Expected a value other than null.' ); } } public static function true($value, $message = '') { if (true !== $value) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to be true. Got: %s', static::valueToString($value) )); } } public static function false($value, $message = '') { if (false !== $value) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to be false. Got: %s', static::valueToString($value) )); } } public static function eq($value, $value2, $message = '') { if ($value2 != $value) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value equal to %2$s. Got: %s', static::valueToString($value), static::valueToString($value2) )); } } public static function notEq($value, $value2, $message = '') { if ($value2 == $value) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a different value than %s.', static::valueToString($value2) )); } } public static function same($value, $value2, $message = '') { if ($value2 !== $value) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value identical to %2$s. Got: %s', static::valueToString($value), static::valueToString($value2) )); } } public static function notSame($value, $value2, $message = '') { if ($value2 === $value) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value not identical to %s.', static::valueToString($value2) )); } } public static function greaterThan($value, $limit, $message = '') { if ($value <= $limit) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value greater than %2$s. Got: %s', static::valueToString($value), static::valueToString($limit) )); } } public static function greaterThanEq($value, $limit, $message = '') { if ($value < $limit) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value greater than or equal to %2$s. Got: %s', static::valueToString($value), static::valueToString($limit) )); } } public static function lessThan($value, $limit, $message = '') { if ($value >= $limit) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value less than %2$s. Got: %s', static::valueToString($value), static::valueToString($limit) )); } } public static function lessThanEq($value, $limit, $message = '') { if ($value > $limit) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value less than or equal to %2$s. Got: %s', static::valueToString($value), static::valueToString($limit) )); } } public static function range($value, $min, $max, $message = '') { if ($value < $min || $value > $max) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value between %2$s and %3$s. Got: %s', static::valueToString($value), static::valueToString($min), static::valueToString($max) )); } } public static function oneOf($value, array $values, $message = '') { if (!in_array($value, $values, true)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected one of: %2$s. Got: %s', static::valueToString($value), implode(', ', array_map(array('static', 'valueToString'), $values)) )); } } public static function contains($value, $subString, $message = '') { if (false === strpos($value, $subString)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to contain %2$s. Got: %s', static::valueToString($value), static::valueToString($subString) )); } } public static function startsWith($value, $prefix, $message = '') { if (0 !== strpos($value, $prefix)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to start with %2$s. Got: %s', static::valueToString($value), static::valueToString($prefix) )); } } public static function startsWithLetter($value, $message = '') { $valid = isset($value[0]); if ($valid) { $locale = setlocale(LC_CTYPE, 0); setlocale(LC_CTYPE, 'C'); $valid = ctype_alpha($value[0]); setlocale(LC_CTYPE, $locale); } if (!$valid) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to start with a letter. Got: %s', static::valueToString($value) )); } } public static function endsWith($value, $suffix, $message = '') { if ($suffix !== substr($value, -static::strlen($suffix))) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to end with %2$s. Got: %s', static::valueToString($value), static::valueToString($suffix) )); } } public static function regex($value, $pattern, $message = '') { if (!preg_match($pattern, $value)) { static::reportInvalidArgument(sprintf( $message ?: 'The value %s does not match the expected pattern.', static::valueToString($value) )); } } public static function alpha($value, $message = '') { $locale = setlocale(LC_CTYPE, 0); setlocale(LC_CTYPE, 'C'); $valid = !ctype_alpha($value); setlocale(LC_CTYPE, $locale); if ($valid) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to contain only letters. Got: %s', static::valueToString($value) )); } } public static function digits($value, $message = '') { $locale = setlocale(LC_CTYPE, 0); setlocale(LC_CTYPE, 'C'); $valid = !ctype_digit($value); setlocale(LC_CTYPE, $locale); if ($valid) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to contain digits only. Got: %s', static::valueToString($value) )); } } public static function alnum($value, $message = '') { $locale = setlocale(LC_CTYPE, 0); setlocale(LC_CTYPE, 'C'); $valid = !ctype_alnum($value); setlocale(LC_CTYPE, $locale); if ($valid) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to contain letters and digits only. Got: %s', static::valueToString($value) )); } } public static function lower($value, $message = '') { $locale = setlocale(LC_CTYPE, 0); setlocale(LC_CTYPE, 'C'); $valid = !ctype_lower($value); setlocale(LC_CTYPE, $locale); if ($valid) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to contain lowercase characters only. Got: %s', static::valueToString($value) )); } } public static function upper($value, $message = '') { $locale = setlocale(LC_CTYPE, 0); setlocale(LC_CTYPE, 'C'); $valid = !ctype_upper($value); setlocale(LC_CTYPE, $locale); if ($valid) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to contain uppercase characters only. Got: %s', static::valueToString($value) )); } } public static function length($value, $length, $message = '') { if ($length !== static::strlen($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to contain %2$s characters. Got: %s', static::valueToString($value), $length )); } } public static function minLength($value, $min, $message = '') { if (static::strlen($value) < $min) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to contain at least %2$s characters. Got: %s', static::valueToString($value), $min )); } } public static function maxLength($value, $max, $message = '') { if (static::strlen($value) > $max) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to contain at most %2$s characters. Got: %s', static::valueToString($value), $max )); } } public static function lengthBetween($value, $min, $max, $message = '') { $length = static::strlen($value); if ($length < $min || $length > $max) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a value to contain between %2$s and %3$s characters. Got: %s', static::valueToString($value), $min, $max )); } } public static function fileExists($value, $message = '') { static::string($value); if (!file_exists($value)) { static::reportInvalidArgument(sprintf( $message ?: 'The file %s does not exist.', static::valueToString($value) )); } } public static function file($value, $message = '') { static::fileExists($value, $message); if (!is_file($value)) { static::reportInvalidArgument(sprintf( $message ?: 'The path %s is not a file.', static::valueToString($value) )); } } public static function directory($value, $message = '') { static::fileExists($value, $message); if (!is_dir($value)) { static::reportInvalidArgument(sprintf( $message ?: 'The path %s is no directory.', static::valueToString($value) )); } } public static function readable($value, $message = '') { if (!is_readable($value)) { static::reportInvalidArgument(sprintf( $message ?: 'The path %s is not readable.', static::valueToString($value) )); } } public static function writable($value, $message = '') { if (!is_writable($value)) { static::reportInvalidArgument(sprintf( $message ?: 'The path %s is not writable.', static::valueToString($value) )); } } public static function classExists($value, $message = '') { if (!class_exists($value)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected an existing class name. Got: %s', static::valueToString($value) )); } } public static function subclassOf($value, $class, $message = '') { if (!is_subclass_of($value, $class)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected a sub-class of %2$s. Got: %s', static::valueToString($value), static::valueToString($class) )); } } public static function implementsInterface($value, $interface, $message = '') { if (!in_array($interface, class_implements($value))) { static::reportInvalidArgument(sprintf( $message ?: 'Expected an implementation of %2$s. Got: %s', static::valueToString($value), static::valueToString($interface) )); } } public static function propertyExists($classOrObject, $property, $message = '') { if (!property_exists($classOrObject, $property)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected the property %s to exist.', static::valueToString($property) )); } } public static function propertyNotExists($classOrObject, $property, $message = '') { if (property_exists($classOrObject, $property)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected the property %s to not exist.', static::valueToString($property) )); } } public static function methodExists($classOrObject, $method, $message = '') { if (!method_exists($classOrObject, $method)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected the method %s to exist.', static::valueToString($method) )); } } public static function methodNotExists($classOrObject, $method, $message = '') { if (method_exists($classOrObject, $method)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected the method %s to not exist.', static::valueToString($method) )); } } public static function keyExists($array, $key, $message = '') { if (!array_key_exists($key, $array)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected the key %s to exist.', static::valueToString($key) )); } } public static function keyNotExists($array, $key, $message = '') { if (array_key_exists($key, $array)) { static::reportInvalidArgument(sprintf( $message ?: 'Expected the key %s to not exist.', static::valueToString($key) )); } } public static function count($array, $number, $message = '') { static::eq( count($array), $number, $message ?: sprintf('Expected an array to contain %d elements. Got: %d.', $number, count($array)) ); } public static function uuid($value, $message = '') { $value = str_replace(array('urn:', 'uuid:', '{', '}'), '', $value); // The nil UUID is special form of UUID that is specified to have all // 128 bits set to zero. if ('00000000-0000-0000-0000-000000000000' === $value) { return; } if (!preg_match('/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/', $value)) { static::reportInvalidArgument(sprintf( $message ?: 'Value %s is not a valid UUID.', static::valueToString($value) )); } } public static function throws(Closure $expression, $class = 'Exception', $message = '') { static::string($class); $actual = 'none'; try { $expression(); } catch (Exception $e) { $actual = get_class($e); if ($e instanceof $class) { return; } } catch (Throwable $e) { $actual = get_class($e); if ($e instanceof $class) { return; } } static::reportInvalidArgument($message ?: sprintf( 'Expected to throw "%s", got "%s"', $class, $actual )); } public static function __callStatic($name, $arguments) { if ('nullOr' === substr($name, 0, 6)) { if (null !== $arguments[0]) { $method = lcfirst(substr($name, 6)); call_user_func_array(array('static', $method), $arguments); } return; } if ('all' === substr($name, 0, 3)) { static::isTraversable($arguments[0]); $method = lcfirst(substr($name, 3)); $args = $arguments; foreach ($arguments[0] as $entry) { $args[0] = $entry; call_user_func_array(array('static', $method), $args); } return; } throw new BadMethodCallException('No such method: '.$name); } protected static function valueToString($value) { if (null === $value) { return 'null'; } if (true === $value) { return 'true'; } if (false === $value) { return 'false'; } if (is_array($value)) { return 'array'; } if (is_object($value)) { return get_class($value); } if (is_resource($value)) { return 'resource'; } if (is_string($value)) { return '"'.$value.'"'; } return (string) $value; } protected static function typeToString($value) { return is_object($value) ? get_class($value) : gettype($value); } protected static function strlen($value) { if (!function_exists('mb_detect_encoding')) { return strlen($value); } if (false === $encoding = mb_detect_encoding($value)) { return strlen($value); } return mb_strwidth($value, $encoding); } protected static function reportInvalidArgument($message) { throw new InvalidArgumentException($message); } private function __construct() { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Webmozart\PathUtil; use InvalidArgumentException; use RuntimeException; use Webmozart\Assert\Assert; /** * Contains utility methods for handling path strings. * * The methods in this class are able to deal with both UNIX and Windows paths * with both forward and backward slashes. All methods return normalized parts * containing only forward slashes and no excess "." and ".." segments. * * @since 1.0 * * @author Bernhard Schussek * @author Thomas Schulz */ final class Path { /** * The number of buffer entries that triggers a cleanup operation. */ const CLEANUP_THRESHOLD = 1250; /** * The buffer size after the cleanup operation. */ const CLEANUP_SIZE = 1000; /** * Buffers input/output of {@link canonicalize()}. * * @var array */ private static $buffer = array(); /** * The size of the buffer. * * @var int */ private static $bufferSize = 0; /** * Canonicalizes the given path. * * During normalization, all slashes are replaced by forward slashes ("/"). * Furthermore, all "." and ".." segments are removed as far as possible. * ".." segments at the beginning of relative paths are not removed. * * ```php * echo Path::canonicalize("\webmozart\puli\..\css\style.css"); * // => /webmozart/css/style.css * * echo Path::canonicalize("../css/./style.css"); * // => ../css/style.css * ``` * * This method is able to deal with both UNIX and Windows paths. * * @param string $path A path string. * * @return string The canonical path. * * @since 1.0 Added method. * @since 2.0 Method now fails if $path is not a string. * @since 2.1 Added support for `~`. */ public static function canonicalize($path) { if ('' === $path) { return ''; } Assert::string($path, 'The path must be a string. Got: %s'); // This method is called by many other methods in this class. Buffer // the canonicalized paths to make up for the severe performance // decrease. if (isset(self::$buffer[$path])) { return self::$buffer[$path]; } // Replace "~" with user's home directory. if ('~' === $path[0]) { $path = static::getHomeDirectory().substr($path, 1); } $path = str_replace('\\', '/', $path); list($root, $pathWithoutRoot) = self::split($path); $parts = explode('/', $pathWithoutRoot); $canonicalParts = array(); // Collapse "." and "..", if possible foreach ($parts as $part) { if ('.' === $part || '' === $part) { continue; } // Collapse ".." with the previous part, if one exists // Don't collapse ".." if the previous part is also ".." if ('..' === $part && count($canonicalParts) > 0 && '..' !== $canonicalParts[count($canonicalParts) - 1]) { array_pop($canonicalParts); continue; } // Only add ".." prefixes for relative paths if ('..' !== $part || '' === $root) { $canonicalParts[] = $part; } } // Add the root directory again self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts); ++self::$bufferSize; // Clean up regularly to prevent memory leaks if (self::$bufferSize > self::CLEANUP_THRESHOLD) { self::$buffer = array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true); self::$bufferSize = self::CLEANUP_SIZE; } return $canonicalPath; } /** * Normalizes the given path. * * During normalization, all slashes are replaced by forward slashes ("/"). * Contrary to {@link canonicalize()}, this method does not remove invalid * or dot path segments. Consequently, it is much more efficient and should * be used whenever the given path is known to be a valid, absolute system * path. * * This method is able to deal with both UNIX and Windows paths. * * @param string $path A path string. * * @return string The normalized path. * * @since 2.2 Added method. */ public static function normalize($path) { Assert::string($path, 'The path must be a string. Got: %s'); return str_replace('\\', '/', $path); } /** * Returns the directory part of the path. * * This method is similar to PHP's dirname(), but handles various cases * where dirname() returns a weird result: * * - dirname() does not accept backslashes on UNIX * - dirname("C:/webmozart") returns "C:", not "C:/" * - dirname("C:/") returns ".", not "C:/" * - dirname("C:") returns ".", not "C:/" * - dirname("webmozart") returns ".", not "" * - dirname() does not canonicalize the result * * This method fixes these shortcomings and behaves like dirname() * otherwise. * * The result is a canonical path. * * @param string $path A path string. * * @return string The canonical directory part. Returns the root directory * if the root directory is passed. Returns an empty string * if a relative path is passed that contains no slashes. * Returns an empty string if an empty string is passed. * * @since 1.0 Added method. * @since 2.0 Method now fails if $path is not a string. */ public static function getDirectory($path) { if ('' === $path) { return ''; } $path = static::canonicalize($path); // Maintain scheme if (false !== ($pos = strpos($path, '://'))) { $scheme = substr($path, 0, $pos + 3); $path = substr($path, $pos + 3); } else { $scheme = ''; } if (false !== ($pos = strrpos($path, '/'))) { // Directory equals root directory "/" if (0 === $pos) { return $scheme.'/'; } // Directory equals Windows root "C:/" if (2 === $pos && ctype_alpha($path[0]) && ':' === $path[1]) { return $scheme.substr($path, 0, 3); } return $scheme.substr($path, 0, $pos); } return ''; } /** * Returns canonical path of the user's home directory. * * Supported operating systems: * * - UNIX * - Windows8 and upper * * If your operation system or environment isn't supported, an exception is thrown. * * The result is a canonical path. * * @return string The canonical home directory * * @throws RuntimeException If your operation system or environment isn't supported * * @since 2.1 Added method. */ public static function getHomeDirectory() { // For UNIX support if (getenv('HOME')) { return static::canonicalize(getenv('HOME')); } // For >= Windows8 support if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) { return static::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH')); } throw new RuntimeException("Your environment or operation system isn't supported"); } /** * Returns the root directory of a path. * * The result is a canonical path. * * @param string $path A path string. * * @return string The canonical root directory. Returns an empty string if * the given path is relative or empty. * * @since 1.0 Added method. * @since 2.0 Method now fails if $path is not a string. */ public static function getRoot($path) { if ('' === $path) { return ''; } Assert::string($path, 'The path must be a string. Got: %s'); // Maintain scheme if (false !== ($pos = strpos($path, '://'))) { $scheme = substr($path, 0, $pos + 3); $path = substr($path, $pos + 3); } else { $scheme = ''; } // UNIX root "/" or "\" (Windows style) if ('/' === $path[0] || '\\' === $path[0]) { return $scheme.'/'; } $length = strlen($path); // Windows root if ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) { // Special case: "C:" if (2 === $length) { return $scheme.$path.'/'; } // Normal case: "C:/ or "C:\" if ('/' === $path[2] || '\\' === $path[2]) { return $scheme.$path[0].$path[1].'/'; } } return ''; } /** * Returns the file name from a file path. * * @param string $path The path string. * * @return string The file name. * * @since 1.1 Added method. * @since 2.0 Method now fails if $path is not a string. */ public static function getFilename($path) { if ('' === $path) { return ''; } Assert::string($path, 'The path must be a string. Got: %s'); return basename($path); } /** * Returns the file name without the extension from a file path. * * @param string $path The path string. * @param string|null $extension If specified, only that extension is cut * off (may contain leading dot). * * @return string The file name without extension. * * @since 1.1 Added method. * @since 2.0 Method now fails if $path or $extension have invalid types. */ public static function getFilenameWithoutExtension($path, $extension = null) { if ('' === $path) { return ''; } Assert::string($path, 'The path must be a string. Got: %s'); Assert::nullOrString($extension, 'The extension must be a string or null. Got: %s'); if (null !== $extension) { // remove extension and trailing dot return rtrim(basename($path, $extension), '.'); } return pathinfo($path, PATHINFO_FILENAME); } /** * Returns the extension from a file path. * * @param string $path The path string. * @param bool $forceLowerCase Forces the extension to be lower-case * (requires mbstring extension for correct * multi-byte character handling in extension). * * @return string The extension of the file path (without leading dot). * * @since 1.1 Added method. * @since 2.0 Method now fails if $path is not a string. */ public static function getExtension($path, $forceLowerCase = false) { if ('' === $path) { return ''; } Assert::string($path, 'The path must be a string. Got: %s'); $extension = pathinfo($path, PATHINFO_EXTENSION); if ($forceLowerCase) { $extension = self::toLower($extension); } return $extension; } /** * Returns whether the path has an extension. * * @param string $path The path string. * @param string|array|null $extensions If null or not provided, checks if * an extension exists, otherwise * checks for the specified extension * or array of extensions (with or * without leading dot). * @param bool $ignoreCase Whether to ignore case-sensitivity * (requires mbstring extension for * correct multi-byte character * handling in the extension). * * @return bool Returns `true` if the path has an (or the specified) * extension and `false` otherwise. * * @since 1.1 Added method. * @since 2.0 Method now fails if $path or $extensions have invalid types. */ public static function hasExtension($path, $extensions = null, $ignoreCase = false) { if ('' === $path) { return false; } $extensions = is_object($extensions) ? array($extensions) : (array) $extensions; Assert::allString($extensions, 'The extensions must be strings. Got: %s'); $actualExtension = self::getExtension($path, $ignoreCase); // Only check if path has any extension if (empty($extensions)) { return '' !== $actualExtension; } foreach ($extensions as $key => $extension) { if ($ignoreCase) { $extension = self::toLower($extension); } // remove leading '.' in extensions array $extensions[$key] = ltrim($extension, '.'); } return in_array($actualExtension, $extensions); } /** * Changes the extension of a path string. * * @param string $path The path string with filename.ext to change. * @param string $extension New extension (with or without leading dot). * * @return string The path string with new file extension. * * @since 1.1 Added method. * @since 2.0 Method now fails if $path or $extension is not a string. */ public static function changeExtension($path, $extension) { if ('' === $path) { return ''; } Assert::string($extension, 'The extension must be a string. Got: %s'); $actualExtension = self::getExtension($path); $extension = ltrim($extension, '.'); // No extension for paths if ('/' === substr($path, -1)) { return $path; } // No actual extension in path if (empty($actualExtension)) { return $path.('.' === substr($path, -1) ? '' : '.').$extension; } return substr($path, 0, -strlen($actualExtension)).$extension; } /** * Returns whether a path is absolute. * * @param string $path A path string. * * @return bool Returns true if the path is absolute, false if it is * relative or empty. * * @since 1.0 Added method. * @since 2.0 Method now fails if $path is not a string. */ public static function isAbsolute($path) { if ('' === $path) { return false; } Assert::string($path, 'The path must be a string. Got: %s'); // Strip scheme if (false !== ($pos = strpos($path, '://'))) { $path = substr($path, $pos + 3); } // UNIX root "/" or "\" (Windows style) if ('/' === $path[0] || '\\' === $path[0]) { return true; } // Windows root if (strlen($path) > 1 && ctype_alpha($path[0]) && ':' === $path[1]) { // Special case: "C:" if (2 === strlen($path)) { return true; } // Normal case: "C:/ or "C:\" if ('/' === $path[2] || '\\' === $path[2]) { return true; } } return false; } /** * Returns whether a path is relative. * * @param string $path A path string. * * @return bool Returns true if the path is relative or empty, false if * it is absolute. * * @since 1.0 Added method. * @since 2.0 Method now fails if $path is not a string. */ public static function isRelative($path) { return !static::isAbsolute($path); } /** * Turns a relative path into an absolute path. * * Usually, the relative path is appended to the given base path. Dot * segments ("." and "..") are removed/collapsed and all slashes turned * into forward slashes. * * ```php * echo Path::makeAbsolute("../style.css", "/webmozart/puli/css"); * // => /webmozart/puli/style.css * ``` * * If an absolute path is passed, that path is returned unless its root * directory is different than the one of the base path. In that case, an * exception is thrown. * * ```php * Path::makeAbsolute("/style.css", "/webmozart/puli/css"); * // => /style.css * * Path::makeAbsolute("C:/style.css", "C:/webmozart/puli/css"); * // => C:/style.css * * Path::makeAbsolute("C:/style.css", "/webmozart/puli/css"); * // InvalidArgumentException * ``` * * If the base path is not an absolute path, an exception is thrown. * * The result is a canonical path. * * @param string $path A path to make absolute. * @param string $basePath An absolute base path. * * @return string An absolute path in canonical form. * * @throws InvalidArgumentException If the base path is not absolute or if * the given path is an absolute path with * a different root than the base path. * * @since 1.0 Added method. * @since 2.0 Method now fails if $path or $basePath is not a string. * @since 2.2.2 Method does not fail anymore of $path and $basePath are * absolute, but on different partitions. */ public static function makeAbsolute($path, $basePath) { Assert::stringNotEmpty($basePath, 'The base path must be a non-empty string. Got: %s'); if (!static::isAbsolute($basePath)) { throw new InvalidArgumentException(sprintf( 'The base path "%s" is not an absolute path.', $basePath )); } if (static::isAbsolute($path)) { return static::canonicalize($path); } if (false !== ($pos = strpos($basePath, '://'))) { $scheme = substr($basePath, 0, $pos + 3); $basePath = substr($basePath, $pos + 3); } else { $scheme = ''; } return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path); } /** * Turns a path into a relative path. * * The relative path is created relative to the given base path: * * ```php * echo Path::makeRelative("/webmozart/style.css", "/webmozart/puli"); * // => ../style.css * ``` * * If a relative path is passed and the base path is absolute, the relative * path is returned unchanged: * * ```php * Path::makeRelative("style.css", "/webmozart/puli/css"); * // => style.css * ``` * * If both paths are relative, the relative path is created with the * assumption that both paths are relative to the same directory: * * ```php * Path::makeRelative("style.css", "webmozart/puli/css"); * // => ../../../style.css * ``` * * If both paths are absolute, their root directory must be the same, * otherwise an exception is thrown: * * ```php * Path::makeRelative("C:/webmozart/style.css", "/webmozart/puli"); * // InvalidArgumentException * ``` * * If the passed path is absolute, but the base path is not, an exception * is thrown as well: * * ```php * Path::makeRelative("/webmozart/style.css", "webmozart/puli"); * // InvalidArgumentException * ``` * * If the base path is not an absolute path, an exception is thrown. * * The result is a canonical path. * * @param string $path A path to make relative. * @param string $basePath A base path. * * @return string A relative path in canonical form. * * @throws InvalidArgumentException If the base path is not absolute or if * the given path has a different root * than the base path. * * @since 1.0 Added method. * @since 2.0 Method now fails if $path or $basePath is not a string. */ public static function makeRelative($path, $basePath) { Assert::string($basePath, 'The base path must be a string. Got: %s'); $path = static::canonicalize($path); $basePath = static::canonicalize($basePath); list($root, $relativePath) = self::split($path); list($baseRoot, $relativeBasePath) = self::split($basePath); // If the base path is given as absolute path and the path is already // relative, consider it to be relative to the given absolute path // already if ('' === $root && '' !== $baseRoot) { // If base path is already in its root if ('' === $relativeBasePath) { $relativePath = ltrim($relativePath, './\\'); } return $relativePath; } // If the passed path is absolute, but the base path is not, we // cannot generate a relative path if ('' !== $root && '' === $baseRoot) { throw new InvalidArgumentException(sprintf( 'The absolute path "%s" cannot be made relative to the '. 'relative path "%s". You should provide an absolute base '. 'path instead.', $path, $basePath )); } // Fail if the roots of the two paths are different if ($baseRoot && $root !== $baseRoot) { throw new InvalidArgumentException(sprintf( 'The path "%s" cannot be made relative to "%s", because they '. 'have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot )); } if ('' === $relativeBasePath) { return $relativePath; } // Build a "../../" prefix with as many "../" parts as necessary $parts = explode('/', $relativePath); $baseParts = explode('/', $relativeBasePath); $dotDotPrefix = ''; // Once we found a non-matching part in the prefix, we need to add // "../" parts for all remaining parts $match = true; foreach ($baseParts as $i => $basePart) { if ($match && isset($parts[$i]) && $basePart === $parts[$i]) { unset($parts[$i]); continue; } $match = false; $dotDotPrefix .= '../'; } return rtrim($dotDotPrefix.implode('/', $parts), '/'); } /** * Returns whether the given path is on the local filesystem. * * @param string $path A path string. * * @return bool Returns true if the path is local, false for a URL. * * @since 1.0 Added method. * @since 2.0 Method now fails if $path is not a string. */ public static function isLocal($path) { Assert::string($path, 'The path must be a string. Got: %s'); return '' !== $path && false === strpos($path, '://'); } /** * Returns the longest common base path of a set of paths. * * Dot segments ("." and "..") are removed/collapsed and all slashes turned * into forward slashes. * * ```php * $basePath = Path::getLongestCommonBasePath(array( * '/webmozart/css/style.css', * '/webmozart/css/..' * )); * // => /webmozart * ``` * * The root is returned if no common base path can be found: * * ```php * $basePath = Path::getLongestCommonBasePath(array( * '/webmozart/css/style.css', * '/puli/css/..' * )); * // => / * ``` * * If the paths are located on different Windows partitions, `null` is * returned. * * ```php * $basePath = Path::getLongestCommonBasePath(array( * 'C:/webmozart/css/style.css', * 'D:/webmozart/css/..' * )); * // => null * ``` * * @param array $paths A list of paths. * * @return string|null The longest common base path in canonical form or * `null` if the paths are on different Windows * partitions. * * @since 1.0 Added method. * @since 2.0 Method now fails if $paths are not strings. */ public static function getLongestCommonBasePath(array $paths) { Assert::allString($paths, 'The paths must be strings. Got: %s'); list($bpRoot, $basePath) = self::split(self::canonicalize(reset($paths))); for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) { list($root, $path) = self::split(self::canonicalize(current($paths))); // If we deal with different roots (e.g. C:/ vs. D:/), it's time // to quit if ($root !== $bpRoot) { return null; } // Make the base path shorter until it fits into path while (true) { if ('.' === $basePath) { // No more base paths $basePath = ''; // Next path continue 2; } // Prevent false positives for common prefixes // see isBasePath() if (0 === strpos($path.'/', $basePath.'/')) { // Next path continue 2; } $basePath = dirname($basePath); } } return $bpRoot.$basePath; } /** * Joins two or more path strings. * * The result is a canonical path. * * @param string[]|string $paths Path parts as parameters or array. * * @return string The joint path. * * @since 2.0 Added method. */ public static function join($paths) { if (!is_array($paths)) { $paths = func_get_args(); } Assert::allString($paths, 'The paths must be strings. Got: %s'); $finalPath = null; $wasScheme = false; foreach ($paths as $path) { $path = (string) $path; if ('' === $path) { continue; } if (null === $finalPath) { // For first part we keep slashes, like '/top', 'C:\' or 'phar://' $finalPath = $path; $wasScheme = (strpos($path, '://') !== false); continue; } // Only add slash if previous part didn't end with '/' or '\' if (!in_array(substr($finalPath, -1), array('/', '\\'))) { $finalPath .= '/'; } // If first part included a scheme like 'phar://' we allow current part to start with '/', otherwise trim $finalPath .= $wasScheme ? $path : ltrim($path, '/'); $wasScheme = false; } if (null === $finalPath) { return ''; } return self::canonicalize($finalPath); } /** * Returns whether a path is a base path of another path. * * Dot segments ("." and "..") are removed/collapsed and all slashes turned * into forward slashes. * * ```php * Path::isBasePath('/webmozart', '/webmozart/css'); * // => true * * Path::isBasePath('/webmozart', '/webmozart'); * // => true * * Path::isBasePath('/webmozart', '/webmozart/..'); * // => false * * Path::isBasePath('/webmozart', '/puli'); * // => false * ``` * * @param string $basePath The base path to test. * @param string $ofPath The other path. * * @return bool Whether the base path is a base path of the other path. * * @since 1.0 Added method. * @since 2.0 Method now fails if $basePath or $ofPath is not a string. */ public static function isBasePath($basePath, $ofPath) { Assert::string($basePath, 'The base path must be a string. Got: %s'); $basePath = self::canonicalize($basePath); $ofPath = self::canonicalize($ofPath); // Append slashes to prevent false positives when two paths have // a common prefix, for example /base/foo and /base/foobar. // Don't append a slash for the root "/", because then that root // won't be discovered as common prefix ("//" is not a prefix of // "/foobar/"). return 0 === strpos($ofPath.'/', rtrim($basePath, '/').'/'); } /** * Splits a part into its root directory and the remainder. * * If the path has no root directory, an empty root directory will be * returned. * * If the root directory is a Windows style partition, the resulting root * will always contain a trailing slash. * * list ($root, $path) = Path::split("C:/webmozart") * // => array("C:/", "webmozart") * * list ($root, $path) = Path::split("C:") * // => array("C:/", "") * * @param string $path The canonical path to split. * * @return string[] An array with the root directory and the remaining * relative path. */ private static function split($path) { if ('' === $path) { return array('', ''); } // Remember scheme as part of the root, if any if (false !== ($pos = strpos($path, '://'))) { $root = substr($path, 0, $pos + 3); $path = substr($path, $pos + 3); } else { $root = ''; } $length = strlen($path); // Remove and remember root directory if ('/' === $path[0]) { $root .= '/'; $path = $length > 1 ? substr($path, 1) : ''; } elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) { if (2 === $length) { // Windows special case: "C:" $root .= $path.'/'; $path = ''; } elseif ('/' === $path[2]) { // Windows normal case: "C:/".. $root .= substr($path, 0, 3); $path = $length > 3 ? substr($path, 3) : ''; } } return array($root, $path); } /** * Converts string to lower-case (multi-byte safe if mbstring is installed). * * @param string $str The string * * @return string Lower case string */ private static function toLower($str) { if (function_exists('mb_strtolower')) { return mb_strtolower($str, mb_detect_encoding($str)); } return strtolower($str); } private function __construct() { } } * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Webmozart\PathUtil; use InvalidArgumentException; use Webmozart\Assert\Assert; /** * Contains utility methods for handling URL strings. * * The methods in this class are able to deal with URLs. * * @since 2.3 * * @author Bernhard Schussek * @author Claudio Zizza */ final class Url { /** * Turns a URL into a relative path. * * The result is a canonical path. This class is using functionality of Path class. * * @see Path * * @param string $url A URL to make relative. * @param string $baseUrl A base URL. * * @return string * * @throws InvalidArgumentException If the URL and base URL does * not match. */ public static function makeRelative($url, $baseUrl) { Assert::string($url, 'The URL must be a string. Got: %s'); Assert::string($baseUrl, 'The base URL must be a string. Got: %s'); Assert::contains($baseUrl, '://', '%s is not an absolute Url.'); list($baseHost, $basePath) = self::split($baseUrl); if (false === strpos($url, '://')) { if (0 === strpos($url, '/')) { $host = $baseHost; } else { $host = ''; } $path = $url; } else { list($host, $path) = self::split($url); } if ('' !== $host && $host !== $baseHost) { throw new InvalidArgumentException(sprintf( 'The URL "%s" cannot be made relative to "%s" since their host names are different.', $host, $baseHost )); } return Path::makeRelative($path, $basePath); } /** * Splits a URL into its host and the path. * * ```php * list ($root, $path) = Path::split("http://example.com/webmozart") * // => array("http://example.com", "/webmozart") * * list ($root, $path) = Path::split("http://example.com") * // => array("http://example.com", "") * ``` * * @param string $url The URL to split. * * @return string[] An array with the host and the path of the URL. * * @throws InvalidArgumentException If $url is not a URL. */ private static function split($url) { $pos = strpos($url, '://'); $scheme = substr($url, 0, $pos + 3); $url = substr($url, $pos + 3); if (false !== ($pos = strpos($url, '/'))) { $host = substr($url, 0, $pos); $url = substr($url, $pos); } else { // No path, only host $host = $url; $url = '/'; } // At this point, we have $scheme, $host and $path $root = $scheme.$host; return array($root, $url); } } 'Backup your code, files, and database into a single file.', 'arguments' => array( 'sites' => 'Optional. Site specifications, delimited by commas. Typically, list subdirectory name(s) under /sites.', ), // Most options on sql-dump should not be shown, so just offer a subset. 'options' => drush_sql_get_table_selection_options() + array( 'description' => 'Describe the archive contents.', 'tags' => 'Add tags to the archive manifest. Delimit multiple by commas.', 'destination' => 'The full path and filename in which the archive should be stored. If omitted, it will be saved to the drush-backups directory and a filename will be generated.', 'overwrite' => 'Do not fail if the destination file exists; overwrite it instead. Default is --no-overwrite.', 'generator' => 'The generator name to store in the MANIFEST file. The default is "Drush archive-dump".', 'generatorversion' => 'The generator version number to store in the MANIFEST file. The default is ' . DRUSH_VERSION . '.', 'pipe' => 'Only print the destination of the archive. Useful for scripts that don\'t pass --destination.', 'preserve-symlinks' => 'Preserve symbolic links.', 'no-core' => 'Exclude Drupal core, so the backup only contains the site specific stuff.', 'tar-options' => 'Options passed thru to the tar command.', ), 'examples' => array( 'drush archive-dump default,example.com,foo.com' => 'Write an archive containing 3 sites in it.', 'drush archive-dump @sites' => 'Save archive containing all sites in a multi-site.', 'drush archive-dump default --destination=/backups/mysite.tar' => 'Save archive to custom location.', 'drush archive-dump --tar-options="--exclude=.git --exclude=sites/default/files"' => 'Omits any .git directories found in the tree as well as sites/default/files.', 'drush archive-dump --tar-options="--exclude=%files"' => 'Placeholder %files is replaced with the real path for the current site, and that path is excluded.', ), 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_SITE, 'aliases' => array('ard', 'archive-backup', 'arb'), ); $items['archive-restore'] = array( 'description' => 'Expand a site archive into a Drupal web site.', 'arguments' => array( 'file' => 'The site archive file that should be expanded.', 'site name' => 'Optional. Which site within the archive you want to restore. Defaults to all.', ), 'required-arguments' => 1, 'options' => array( 'destination' => 'Specify where the Drupal site should be expanded, including the docroot. Defaults to the current working directory.', 'db-prefix' => 'An optional table prefix to use during restore.', 'db-url' => array( 'description' => 'A Drupal 6 style database URL indicating the target for database restore. If not provided, the archived settings.php is used.', 'example-value' => 'mysql://root:pass@host/db', ), 'db-su' => 'Account to use when creating the new database. Optional.', 'db-su-pw' => 'Password for the "db-su" account. Optional.', 'overwrite' => 'Allow drush to overwrite any files in the destination. Default is --no-overwrite.', 'tar-options' => 'Options passed thru to the tar command.', ), 'examples' => array( 'drush archive-restore ./example.tar.gz' => 'Restore the files and databases for all sites in the archive.', 'drush archive-restore ./example.tar.gz example.com' => 'Restore the files and database for example.com site.', 'drush archive-restore ./example.tar.gz --destination=/var/www/example.com/docroot' => 'Restore archive to a custom location.', 'drush archive-restore ./example.tar.gz --db-url=mysql://root:pass@127.0.0.1/dbname' => 'Restore archive to a new database (and customize settings.php to point there.).', ), 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'aliases' => array('arr'), ); return $items; } /** * Command callback. Generate site archive file. */ function drush_archive_dump($sites_subdirs = '@self') { $include_platform = !drush_get_option('no-core', FALSE); $tar = drush_get_tar_executable(); $sites = array(); list($aliases, $not_found) = drush_sitealias_resolve_sitespecs(explode(',', $sites_subdirs)); if (!empty($not_found)) { drush_log(dt("Some site subdirectories are not valid Drupal sites: @list", array("@list" => implode(', ', $not_found))), LogLevel::WARNING); } foreach ($aliases as $key => $alias) { $sites[$key] = $alias; if (($db_record = sitealias_get_databases_from_record($alias))) { $sites[$key]['databases'] = $db_record; } else { $sites[$key]['databases'] = array(); drush_log(dt('DB definition not found for !alias', array('!alias' => $key)), LogLevel::NOTICE); } } // The user can specify a destination filepath or not. That filepath might // end with .gz, .tgz, or something else. At the end of this command we will // gzip a file, and we want it to end up with the user-specified name (if // any), but gzip renames files and refuses to compress files ending with // .gz and .tgz, making our lives difficult. Solution: // // 1. Create a unique temporary base name to which gzip WILL append .gz. // 2. If no destination is provided, set $dest_dir to a backup directory and // $final_destination to be the unique name in that dir. // 3. If a destination is provided, set $dest_dir to that directory and // $final_destination to the exact name given. // 4. Set $destination, the actual working file we will build up, to the // unqiue name in $dest_dir. // 5. After gzip'ing $destination, rename $destination.gz to // $final_destination. // // Sheesh. // Create the unique temporary name. $prefix = 'none'; if (!empty($sites)) { $first = current($sites); if ( !empty($first['databases']['default']['default']['database']) ) { $prefix = count($sites) > 1 ? 'multiple_sites' : str_replace('/', '-', $first['databases']['default']['default']['database']); } } $date = gmdate('Ymd_His'); $temp_dest_name = "$prefix.$date.tar"; $final_destination = drush_get_option('destination'); if (!$final_destination) { // No destination provided. $backup = drush_include_engine('version_control', 'backup'); // TODO: this standard Drush pattern leads to a slightly obtuse directory structure. $dest_dir = $backup->prepare_backup_dir('archive-dump'); if (empty($dest_dir)) { $dest_dir = drush_tempdir(); } $final_destination = "$dest_dir/$temp_dest_name.gz"; } else { // Use the supplied --destination. If it is relative, resolve it // relative to the directory in which drush was invoked. $command_cwd = getcwd(); drush_op('chdir', drush_get_context('DRUSH_OLDCWD', getcwd())); // This doesn't perform realpath on the basename, but that's okay. This is // not path-based security. We just use it for checking for perms later. drush_mkdir(dirname($final_destination)); $dest_dir = realpath(dirname($final_destination)); $final_destination = $dest_dir . '/' . basename($final_destination); drush_op('chdir', $command_cwd); } // $dest_dir is either the backup directory or specified directory. Set our // working file. $destination = "$dest_dir/$temp_dest_name"; // Validate the FINAL destination. It should be a file that does not exist // (unless --overwrite) in a writable directory (and a writable file if // it exists). We check all this up front to avoid failing after a long // dump process. $overwrite = drush_get_option('overwrite'); $dest_dir = dirname($final_destination); $dt_args = array('!file' => $final_destination, '!dir' => $dest_dir); if (is_dir($final_destination)) { drush_set_error('DRUSH_ARCHIVE_DEST_IS_DIR', dt('Destination !file must be a file, not a directory.', $dt_args)); return; } else if (file_exists($final_destination)) { if (!$overwrite) { drush_set_error('DRUSH_ARCHIVE_DEST_EXISTS', dt('Destination !file exists; specify --overwrite to overwrite.', $dt_args)); return; } else if (!is_writable($final_destination)) { drush_set_error('DRUSH_ARCHIVE_DEST_FILE_NOT_WRITEABLE', dt('Destination !file is not writable.', $dt_args)); return; } } else if (!is_writable($dest_dir)) { drush_set_error('DRUSH_ARCHIVE_DEST_DIR_NOT_WRITEABLE', dt('Destination directory !dir is not writable.', $dt_args)); return; } // Get the extra options for tar, if any $tar_extra_options = drush_sitealias_evaluate_paths_in_options(drush_get_option('tar-options', '')); // Start adding codebase to the archive. $docroot_path = realpath(drush_get_context('DRUSH_DRUPAL_ROOT')); $docroot = basename($docroot_path); $workdir = dirname($docroot_path); if ($include_platform) { $dereference = (drush_get_option('preserve-symlinks', FALSE)) ? '' : '--dereference '; // Convert destination path to Unix style for tar on MinGW - see http://drupal.org/node/1844224 if (drush_is_mingw()) { $destination_orig = $destination; $destination = str_replace('\\', '/', $destination); $destination = preg_replace('$^([a-zA-Z]):$', '/$1', $destination); } // Archive Drupal core, excluding sites dir. drush_shell_cd_and_exec($workdir, "$tar {$tar_extra_options} --exclude \"{$docroot}/sites\" {$dereference}-cf %s %s", $destination, $docroot); // Add sites/all to the same archive. drush_shell_cd_and_exec($workdir, "$tar {$tar_extra_options} {$dereference}-rf %s %s", $destination, "{$docroot}/sites/all"); // Add special files in sites/ to the archive. Last 2 items are new in Drupal8. $files_to_add = array('sites/README.txt', 'sites/sites.php', 'sites/example.sites.php', 'sites/development.services.yml', 'sites/example.settings.local.php'); foreach ($files_to_add as $file_to_add) { if (file_exists($file_to_add)) { drush_shell_cd_and_exec($workdir, "$tar {$dereference}-rf %s %s", $destination, $docroot . '/' . $file_to_add); } } } $tmp = drush_tempdir(); $all_dbs = array(); // Dump the default database for each site and add to the archive. foreach ($sites as $key => $alias) { if (isset($alias['databases']['default']['default'])) { $db_spec = $alias['databases']['default']['default']; // Use a subdirectory name matching the docroot name. drush_mkdir("{$tmp}/{$docroot}"); // Ensure uniqueness by prefixing key if needed. Remove path delimiters. $dbname = str_replace(DIRECTORY_SEPARATOR, '-', $db_spec['database']); $result_file = count($sites) == 1 ? "$tmp/$dbname.sql" : str_replace('@', '', "$tmp/$key-$dbname.sql"); $all_dbs[$key] = array( 'file' => basename($result_file), 'driver' => $db_spec['driver'], ); $sql = drush_sql_get_class($db_spec); $sql->dump($result_file); drush_shell_cd_and_exec($tmp, "$tar {$tar_extra_options} --dereference -rf %s %s", $destination, basename($result_file)); } } // Build a manifest file AND add sites/$subdir to archive as we go. $platform = array( 'datestamp' => time(), 'formatversion' => '1.0', 'generator' => drush_get_option('generator', 'Drush archive-dump'), 'generatorversion' => drush_get_option('generatorversion', DRUSH_VERSION), 'description' => drush_get_option('description', ''), 'tags' => drush_get_option('tags', ''), 'archiveformat' => ($include_platform ? 'platform' : 'site'), ); $contents = drush_export_ini(array('Global' => $platform)); $i=0; foreach ($sites as $key => $alias) { $status = drush_invoke_process($alias, 'core-status', array(), array(), array('integrate' => FALSE)); if (!$status || $status['error_log']) { drush_log(dt('Unable to determine sites directory for !alias', array('!alias' => $key)), LogLevel::WARNING); } // Add the site specific directory to archive. if (!empty($status['object']['%paths']['%site'])) { drush_shell_cd_and_exec($workdir, "$tar {$tar_extra_options} --dereference -rf %s %s", $destination, "{$docroot}/sites/" . basename($status['object']['%paths']['%site'])); } $site = array( 'docroot' => DRUPAL_ROOT, 'sitedir' => @$status['object']['%paths']['%site'], 'files-public' => @$status['object']['%paths']['%files'], 'files-private' => @$status['object']['%paths']['%private'], ); $site["database-default-file"] = $all_dbs[$key]['file']; $site["database-default-driver"] = $all_dbs[$key]['driver']; // The section title is the sites subdirectory name. $info[basename($site['sitedir'])] = $site; $contents .= "\n" . drush_export_ini($info); unset($info); $i++; } file_put_contents("{$tmp}/MANIFEST.ini", $contents); // Add manifest to archive. drush_shell_cd_and_exec($tmp, "$tar --dereference -rf %s %s", $destination, 'MANIFEST.ini'); // Ensure that default/default.settings.php is in the archive. This is needed // by site-install when restoring a site without any DB. // NOTE: Windows tar file replace operation is broken so we have to check if file already exists. // Otherwise it will corrupt the archive. $res = drush_shell_cd_and_exec($workdir, "$tar -tf %s %s", $destination, $docroot . '/sites/default/default.settings.php'); $output = drush_shell_exec_output(); if (!$res || !isset($output[0]) || empty($output[0])) { drush_shell_cd_and_exec($workdir, "$tar --dereference -vrf %s %s", $destination, $docroot . '/sites/default/default.settings.php'); } // Switch back to original destination in case it was modified for tar on MinGW. if (!empty($destination_orig)) { $destination = $destination_orig; } // Compress the archive if (!drush_shell_exec("gzip --no-name -f %s", $destination)) { // Clean up the tar file drush_register_file_for_deletion($destination); return drush_set_error(DRUSH_APPLICATION_ERROR, dt('Failed to gzip !destination', ['!destination' => $destination])); } // gzip appends .gz unless the name already ends in .gz, .tgz, or .taz. if ("{$destination}.gz" != $final_destination) { drush_move_dir("{$destination}.gz", $final_destination, $overwrite); } drush_log(dt('Archive saved to !dest', array('!dest' => $final_destination)), LogLevel::OK); return $final_destination; } /** * Command argument complete callback. * * @return * List of site names/aliases for archival. */ function archive_archive_dump_complete() { return array('values' => array_keys(_drush_sitealias_all_list())); } /** * Command callback. Restore web site(s) from a site archive file. */ function drush_archive_restore($file, $site_id = NULL) { $tmp = drush_tempdir(); // Get the extra options for tar, if any $tar_extra_options = drush_sitealias_evaluate_paths_in_options(drush_get_option('tar-options', '')); if (!$files = drush_tarball_extract($file, $tmp, FALSE, $tar_extra_options)) { return drush_set_error('DRUSH_ARCHIVE_UNABLE_TO_EXTRACT', dt('Unable to extract site archive tarball to !tmp.', array('!tmp' => $tmp))); } $manifest = $tmp . '/MANIFEST.ini'; if (file_exists($manifest)) { if (!$ini = parse_ini_file($manifest, TRUE)) { return drush_set_error('DRUSH_ARCHIVE_UNABLE_TO_PARSE_MANIFEST', dt('Unable to parse MANIFEST.ini in the archive.')); } } else { $ini = drush_archive_guess_manifest($tmp); } // Backward compatibility: 'archiveformat' did not exist // in older versions of archive-dump. if (!isset( $ini['Global']['archiveformat'])) { $ini['Global']['archiveformat'] = 'platform'; } // Grab the first site in the Manifest and move docroot to destination. $ini_tmp = $ini; unset($ini_tmp['Global']); $first = array_shift($ini_tmp); $docroot = basename($first['docroot']); $destination = drush_get_option('destination', realpath('.') . "/$docroot"); if ($ini['Global']['archiveformat'] == 'platform') { // Move the whole platform in-place at once. if (!drush_move_dir("$tmp/$docroot", $destination, drush_get_option('overwrite'))) { return drush_set_error('DRUSH_ARCHIVE_UNABLE_TO_RESTORE_FILES', dt('Unable to restore platform to !dest', array('!dest' => $destination))); } } else { // When no platform is included we do this on a per-site basis. } // Loop over sites and restore databases and append to settings.php. foreach ($ini as $section => $site) { if ($section != 'Global' && (!isset($site_id) || $section == $site_id) && !empty($site['database-default-file'])) { $site_destination = $destination . '/' . $site['sitedir']; // Restore site, in case not already done above. if ($ini['Global']['archiveformat'] == 'site') { if (!drush_move_dir("$tmp/$docroot/" . $site['sitedir'], $site_destination, drush_get_option('overwrite'))) { return drush_set_error('DRUSH_ARCHIVE_UNABLE_TO_RESTORE_FILES', dt('Unable to restore site to !dest', array('!dest' => $site_destination))); } } // Restore database. $sql_file = $tmp . '/' . $site['database-default-file']; if ($db_url = drush_get_option('db-url')) { if (empty($site_id) && count($ini) >= 3) { // TODO: Use drushrc to provide multiple db-urls for multi-restore? return drush_set_error('DRUSH_ARCHIVE_UNABLE_MULTI_RESTORE', dt('You must specify a site to restore when the archive contains multiple sites and a db-url is provided.')); } $db_spec = drush_convert_db_from_db_url($db_url); } else { $site_specification = $destination . '#' . $section; if ($return = drush_invoke_process($site_specification, 'sql-conf', array(), array('all' => TRUE), array('integrate' => FALSE, 'override-simulated' => TRUE))) { $databases = $return['object']; $db_spec = $databases['default']['default']; } else { return drush_set_error('DRUSH_ARCHIVE_UNABLE_DISCOVER_DB', dt('Unable to get database details from db-url option or settings.php', array('!key' => $key))); } } $sql = drush_sql_get_class($db_spec); $sql->drop_or_create(); $sql->query(NULL, $sql_file); // Append new DB info to settings.php. if ($db_url) { $settingsfile = $destination . '/' . $site['sitedir'] . '/settings.php'; //If settings.php doesn't exist in the archive, create it from default.settings.php. if (!file_exists($settingsfile)) { drush_op('copy', $destination . '/sites/default/default.settings.php', $settingsfile); } // Need to do something here or else we can't write. chmod($settingsfile, 0664); file_put_contents($settingsfile, "\n// Appended by drush archive-restore command.\n", FILE_APPEND); if (drush_drupal_major_version($destination) >= 7) { file_put_contents($settingsfile, "\n" . '$databases = ' . var_export(drush_sitealias_convert_db_from_db_url($db_url), TRUE) . ";\n", FILE_APPEND); } else { file_put_contents($settingsfile, "\n" . '$db_url = \'' . $db_url . "';\n", FILE_APPEND); } drush_log(dt('Drush appended the new database configuration at settings.php. Optionally remove the old configuration manually.'), LogLevel::OK); } } } drush_log(dt('Archive restored to !dest', array('!dest' => $destination)), LogLevel::OK); return $destination; } /** * Command argument complete callback. * * @return * Strong glob of files to complete on. */ function archive_archive_restore_complete() { return array( 'files' => array( 'directories' => array( 'pattern' => '*', 'flags' => GLOB_ONLYDIR, ), 'tar' => array( 'pattern' => '*.tar.gz', ), ), ); } /** * Try to find docroot and DB dump file in an extracted archive. * * @param string $path The location of the extracted archive. * @return array The manifest data. */ function drush_archive_guess_manifest($path) { $db_file = drush_scan_directory($path, '/\.sql$/', array('.', '..', 'CVS'), 0, 0); if (file_exists($path . '/index.php')) { $docroot = './'; } else { $directories = glob($path . '/*' , GLOB_ONLYDIR); $docroot = reset($directories); } $ini = array( 'Global' => array( // Very crude detection of a platform... 'archiveformat' => (drush_drupal_version($docroot) ? 'platform' : 'site'), ), 'default' => array( 'docroot' => $docroot, 'sitedir' => 'sites/default', 'database-default-file' => key($db_file), ), ); return $ini; } 'Fetch a cached object and display it.', 'examples' => array( 'drush cache-get schema' => 'Display the data for the cache id "schema" from the "cache" bin.', 'drush cache-get update_available_releases update' => 'Display the data for the cache id "update_available_releases" from the "update" bin.', ), 'arguments' => array( 'cid' => 'The id of the object to fetch.', 'bin' => 'Optional. The cache bin to fetch from.', ), 'required-arguments' => 1, 'callback' => 'drush_cache_command_get', 'outputformat' => array( 'default' => 'print-r', 'pipe-format' => 'var_export', 'output-data-type' => TRUE, ), 'aliases' => array('cg'), ); $items['cache-clear'] = array( 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'description' => 'Clear a specific cache, or all drupal caches.', 'arguments' => array( 'type' => 'The particular cache to clear. Omit this argument to choose from available caches.', ), 'callback' => 'drush_cache_command_clear', 'aliases' => array('cc'), ); $items['cache-set'] = array( 'description' => 'Cache an object expressed in JSON or var_export() format.', 'arguments' => array( 'cid' => 'The id of the object to set.', 'data' => 'The object to set in the cache. Use \'-\' to read the object from STDIN.', 'bin' => 'Optional. The cache bin to store the object in.', 'expire' => 'Optional. CACHE_PERMANENT, CACHE_TEMPORARY, or a Unix timestamp.', 'tags' => 'An array of cache tags.', ), 'required-arguments' => 2, 'options' => array( // Note that this is not an outputformat option. 'format' => 'Format to parse the object. Use "string" for string (default), and "json" for JSON.', 'cache-get' => 'If the object is the result a previous fetch from the cache, only store the value in the "data" property of the object in the cache.', ), 'callback' => 'drush_cache_command_set', 'aliases' => array('cs'), ); $items['cache-rebuild'] = array( 'description' => 'Rebuild a Drupal 8 site and clear all its caches.', 'options' => array(), 'arguments' => array(), // Bootstrap to DRUSH_BOOTSTAP_DRUPAL_SITE to pick the correct site. // Further bootstrap is done by the rebuild script. 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_SITE, 'core' => array('8+'), 'aliases' => array('cr', 'rebuild'), ); return $items; } /** * Command argument complete callback. * * @return * Array of clear types. */ function cache_cache_clear_complete() { // Bootstrap as far as possible so that Views and others can list their caches. drush_bootstrap_max(); return array('values' => array_keys(drush_cache_clear_types(TRUE))); } function drush_cache_clear_pre_validate($type = NULL) { $types = drush_cache_clear_types(drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)); // Check if the provided type ($type) is a valid cache type. if ($type && !array_key_exists($type, $types)) { if ($type === 'all' && drush_drupal_major_version() >= 8) { return drush_set_error(dt('`cache-clear all` is deprecated for Drupal 8 and later. Please use the `cache-rebuild` command instead.')); } // If we haven't done a full bootstrap, provide a more // specific message with instructions to the user on // bootstrapping a Drupal site for more options. if (!drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { $all_types = drush_cache_clear_types(TRUE); if (array_key_exists($type, $all_types)) { return drush_set_error(dt("'!type' cache requires a working Drupal site to operate on. Use the --root and --uri options, or a site @alias, or cd to a directory containing a Drupal settings.php file.", array('!type' => $type))); } else { return drush_set_error(dt("'!type' cache is not a valid cache type. There may be more cache types available if you select a working Drupal site.", array('!type' => $type))); } } return drush_set_error(dt("'!type' cache is not a valid cache type.", array('!type' => $type))); } } /** * Command callback for drush cache-clear. */ function drush_cache_command_clear($type = NULL) { if (!drush_get_option('cache-clear', TRUE)) { drush_log(dt("Skipping cache-clear operation due to --cache-clear=0 option."), LogLevel::OK); return TRUE; } $types = drush_cache_clear_types(drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)); if (!isset($type)) { // Don't offer 'all' unless Drush has bootstrapped the Drupal site if (!drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { unset($types['all']); } $type = drush_choice($types, 'Enter a number to choose which cache to clear.', '!key'); if (empty($type)) { return drush_user_abort(); } } // Do it. drush_op($types[$type]); if ($type == 'all' && !drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { drush_log(dt("No Drupal site found, only 'drush' cache was cleared."), LogLevel::WARNING); } else { drush_log(dt("'!name' cache was cleared.", array('!name' => $type)), LogLevel::SUCCESS); } } /** * Print an object returned from the cache. * * @param $cid * The cache ID of the object to fetch. * @param $bin * A specific bin to fetch from. If not specified, the default bin is used. */ function drush_cache_command_get($cid = NULL, $bin = NULL) { drush_include_engine('drupal', 'cache'); $result = drush_op('_drush_cache_command_get', $cid, $bin); if (empty($result)) { return drush_set_error('DRUSH_CACHE_OBJECT_NOT_FOUND', dt('The !cid object in the !bin bin was not found.', array('!cid' => $cid, '!bin' => $bin ? $bin : _drush_cache_bin_default()))); } return $result; } /** * Set an object in the cache. * * @param $cid * The cache ID of the object to fetch. * @param $data * The data to save to the cache, or '-' to read from STDIN. * @param $bin * A specific bin to fetch from. If not specified, the default bin is used. * @param $expire * The expiry timestamp for the cached object. * @param $tags * Cache tags for the cached object. */ function drush_cache_command_set($cid = NULL, $data = '', $bin = NULL, $expire = NULL, $tags = array()) { // In addition to prepare, this also validates. Can't easily be in own validate callback as // reading once from STDIN empties it. $data = drush_cache_set_prepare_data($data); if ($data === FALSE && drush_get_error()) { // An error was logged above. return; } drush_include_engine('drupal', 'cache'); return drush_op('_drush_cache_command_set', $cid, $data, $bin, $expire, $tags); } function drush_cache_set_prepare_data($data) { if ($data == '-') { $data = file_get_contents("php://stdin"); } // Now, we parse the object. switch (drush_get_option('format', 'string')) { case 'json': $data = drush_json_decode($data); break; } if (drush_get_option('cache-get')) { // $data might be an object. if (is_object($data) && $data->data) { $data = $data->data; } // But $data returned from `drush cache-get --format=json` will be an array. elseif (is_array($data) && isset($data['data'])) { $data = $data['data']; } else { // If $data is neither object nor array and cache-get was specified, then // there is a problem. return drush_set_error('CACHE_INVALID_FORMAT', dt("'cache-get' was specified as an option, but the data is neither an object or an array.")); } } return $data; } /** * All types of caches available for clearing. Contrib commands can alter in their own. */ function drush_cache_clear_types($include_bootstrapped_types = FALSE) { drush_include_engine('drupal', 'cache'); $types = _drush_cache_clear_types($include_bootstrapped_types); // Include the appropriate environment engine, so callbacks can use core // version specific cache clearing functions directly. drush_include_engine('drupal', 'environment'); // Command files may customize $types as desired. drush_command_invoke_all_ref('drush_cache_clear', $types, $include_bootstrapped_types); return $types; } /** * Clear caches internal to drush core. */ function drush_cache_clear_drush() { drush_cache_clear_all(NULL, 'default'); // commandfiles, etc. drush_cache_clear_all(NULL, 'complete'); // completion // Release XML. We don't clear tarballs since those never change. $matches = drush_scan_directory(drush_directory_cache('download'), "/^https---updates.drupal.org-release-history/", array('.', '..')); array_map('unlink', array_keys($matches)); } /** * Rebuild a Drupal 8 site. * * This is a transpose of core/rebuild.php. Additionally * it also clears drush cache and drupal render cache. */ function drush_cache_rebuild() { if (!drush_get_option('cache-clear', TRUE)) { drush_log(dt("Skipping cache-clear operation due to --cache-clear=0 option."), LogLevel::OK); return TRUE; } chdir(DRUPAL_ROOT); // Clear the APC cache to ensure APC class loader is reset. if (function_exists('apc_fetch')) { apc_clear_cache('user'); } // Clear user cache for all major platforms. $user_caches = [ 'apcu_clear_cache', 'wincache_ucache_clear', 'xcache_clear_cache', ]; foreach (array_filter($user_caches, 'is_callable') as $cache) { call_user_func($cache); } $autoloader = drush_drupal_load_autoloader(DRUPAL_ROOT); require_once DRUSH_DRUPAL_CORE . '/includes/utility.inc'; $request = Request::createFromGlobals(); // Ensure that the HTTP method is set, which does not happen with Request::createFromGlobals(). $request->setMethod('GET'); // Manually resemble early bootstrap of DrupalKernel::boot(). require_once DRUSH_DRUPAL_CORE . '/includes/bootstrap.inc'; DrupalKernel::bootEnvironment(); // Avoid 'Only variables should be passed by reference' $root = DRUPAL_ROOT; $site_path = DrupalKernel::findSitePath($request); Settings::initialize($root, $site_path, $autoloader); // Use our error handler since _drupal_log_error() depends on an unavailable theme system (ugh). set_error_handler('drush_error_handler'); // drupal_rebuild() calls drupal_flush_all_caches() itself, so we don't do it manually. drupal_rebuild($autoloader, $request); drush_log(dt('Cache rebuild complete.'), LogLevel::OK); // As this command replaces `drush cache-clear all` for Drupal 8 users, clear // the Drush cache as well, for consistency with that behavior. drush_cache_clear_drush(); } 'Open an interactive shell on a Drupal site.', 'remote-tty' => TRUE, 'aliases' => array('php'), 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'topics' => array('docs-repl'), 'options' => array( 'version-history' => 'Use command history based on Drupal version (Default is per site).', ), ); $items['docs-repl'] = array( 'description' => 'repl.md', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array(drush_get_context('DOC_PREFIX', DRUSH_BASE_PATH) . '/docs/repl.md'), ); return $items; } /** * Command callback. */ function drush_cli_core_cli() { $drupal_major_version = drush_drupal_major_version(); $configuration = new \Psy\Configuration(); // Set the Drush specific history file path. $configuration->setHistoryFile(drush_history_path_cli()); // Disable checking for updates. Our dependencies are managed with Composer. $configuration->setUpdateCheck(Checker::NEVER); $shell = new Shell($configuration); if ($drupal_major_version >= 8) { // Register the assertion handler so exceptions are thrown instead of errors // being triggered. This plays nicer with PsySH. Handle::register(); $shell->setScopeVariables(['container' => \Drupal::getContainer()]); // Add Drupal 8 specific casters to the shell configuration. $configuration->addCasters(_drush_core_cli_get_casters()); } // Add Drush commands to the shell. $commands = [new DrushHelpCommand()]; foreach (drush_commands_categorize(_drush_core_cli_get_commands()) as $category_data) { $category_title = (string) $category_data['title']; foreach ($category_data['commands'] as $command_config) { $command = new DrushCommand($command_config); // Set the category label on each. $command->setCategory($category_title); $commands[] = $command; } } $shell->addCommands($commands); // PsySH will never return control to us, but our shutdown handler will still // run after the user presses ^D. Mark this command as completed to avoid a // spurious error message. drush_set_context('DRUSH_EXECUTION_COMPLETED', TRUE); // Run the terminate event before the shell is run. Otherwise, if the shell // is forking processes (the default), any child processes will close the // database connection when they are killed. So when we return back to the // parent process after, there is no connection. This will be called after the // command in preflight still, but the subscriber instances are already // created from before. Call terminate() regardless, this is a no-op for all // DrupalBoot classes except DrupalBoot8. if ($bootstrap = drush_get_bootstrap_object()) { $bootstrap->terminate(); } // To fix the above problem in Drupal 7, the connection can be closed manually. // This will make sure a new connection is created again in child loops. So // any shutdown functions will still run ok after the shell has exited. if ($drupal_major_version == 7) { Database::closeConnection(); } $shell->run(); } /** * Returns a filtered list of Drush commands used for CLI commands. * * @return array */ function _drush_core_cli_get_commands() { $commands = drush_get_commands(); $ignored_commands = ['help', 'drush-psysh', 'php-eval', 'core-cli', 'php']; $php_keywords = _drush_core_cli_get_php_keywords(); foreach ($commands as $name => $config) { // Ignore some commands that don't make sense inside PsySH, are PHP keywords // are hidden, or are aliases. if (in_array($name, $ignored_commands) || in_array($name, $php_keywords) || !empty($config['hidden']) || ($name !== $config['command'])) { unset($commands[$name]); } else { // Make sure the command aliases don't contain any PHP keywords. if (!empty($config['aliases'])) { $commands[$name]['aliases'] = array_diff($commands[$name]['aliases'], $php_keywords); } } } return $commands; } /** * Returns a mapped array of casters for use in the shell. * * These are Symfony VarDumper casters. * See http://symfony.com/doc/current/components/var_dumper/advanced.html#casters * for more information. * * @return array. * An array of caster callbacks keyed by class or interface. */ function _drush_core_cli_get_casters() { return [ 'Drupal\Core\Entity\ContentEntityInterface' => 'Drush\Psysh\Caster::castContentEntity', 'Drupal\Core\Field\FieldItemListInterface' => 'Drush\Psysh\Caster::castFieldItemList', 'Drupal\Core\Field\FieldItemInterface' => 'Drush\Psysh\Caster::castFieldItem', 'Drupal\Core\Config\Entity\ConfigEntityInterface' => 'Drush\Psysh\Caster::castConfigEntity', 'Drupal\Core\Config\ConfigBase' => 'Drush\Psysh\Caster::castConfig', 'Drupal\Component\DependencyInjection\Container' => 'Drush\Psysh\Caster::castContainer', ]; } /** * Returns the file path for the CLI history. * * This can either be site specific (default) or Drupal version specific. * * @return string. */ function drush_history_path_cli() { $cli_directory = drush_directory_cache('cli'); // If only the Drupal version is being used for the history. if (drush_get_option('version-history', FALSE)) { $drupal_major_version = drush_drupal_major_version(); $file_name = "drupal-$drupal_major_version"; } // If there is an alias, use that in the site specific name. Otherwise, // use a hash of the root path. else { if ($alias = drush_get_context('DRUSH_TARGET_SITE_ALIAS')) { $site = drush_sitealias_get_record($alias); $site_suffix = $site['#name']; } else { $site_suffix = md5(DRUPAL_ROOT); } $file_name = "drupal-site-$site_suffix"; } $full_path = "$cli_directory/$file_name"; // Output the history path if verbose is enabled. if (drush_get_context('DRUSH_VERBOSE')) { drush_log(dt('History: @full_path', ['@full_path' => $full_path]), LogLevel::INFO); } return $full_path; } /** * Returns a list of PHP keywords. * * This will act as a blacklist for command and alias names. * * @return array */ function _drush_core_cli_get_php_keywords() { return [ '__halt_compiler', 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch', 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements', 'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new', 'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', ]; } array('config')); $items['config-get'] = array( 'description' => 'Display a config value, or a whole configuration object.', 'arguments' => array( 'config-name' => 'The config object name, for example "system.site".', 'key' => 'The config key, for example "page.front". Optional.', ), 'required-arguments' => 1, 'options' => array( 'source' => array( 'description' => 'The config storage source to read. Additional labels may be defined in settings.php', 'example-value' => 'sync', 'value' => 'required', ), 'include-overridden' => array( 'description' => 'Include overridden values.', ) ), 'examples' => array( 'drush config-get system.site' => 'Displays the system.site config.', 'drush config-get system.site page.front' => 'gets system.site:page.front value.', ), 'outputformat' => array( 'default' => 'yaml', 'pipe-format' => 'var_export', ), 'aliases' => array('cget'), 'core' => array('8+'), ); $items['config-set'] = array( 'description' => 'Set config value directly. Does not perform a config import.', 'arguments' => array( 'config-name' => 'The config object name, for example "system.site".', 'key' => 'The config key, for example "page.front".', 'value' => 'The value to assign to the config key. Use \'-\' to read from STDIN.', ), 'options' => array( 'format' => array( 'description' => 'Format to parse the object. Use "string" for string (default), and "yaml" for YAML.', 'example-value' => 'yaml', 'value' => 'required', ), // A convenient way to pass a multiline value within a backend request. 'value' => array( 'description' => 'The value to assign to the config key (if any).', 'hidden' => TRUE, ), ), 'examples' => array( 'drush config-set system.site page.front node' => 'Sets system.site:page.front to "node".', ), 'aliases' => array('cset'), 'core' => array('8+'), ); $items['config-export'] = array( 'description' => 'Export configuration to a directory.', 'core' => array('8+'), 'aliases' => array('cex'), 'arguments' => array( 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'sync'", ), 'options' => array( 'add' => 'Run `git add -p` after exporting. This lets you choose which config changes to sync for commit.', 'commit' => 'Run `git add -A` and `git commit` after exporting. This commits everything that was exported without prompting.', 'message' => 'Commit comment for the exported configuration. Optional; may only be used with --commit or --push.', 'push' => 'Run `git push` after committing. Implies --commit.', 'remote' => array( 'description' => 'The remote git branch to use to push changes. Defaults to "origin".', 'example-value' => 'origin', ), 'branch' => array( 'description' => 'Make commit on provided working branch. Ignored if used without --commit or --push.', 'example-value' => 'branchname', ), 'destination' => 'An arbitrary directory that should receive the exported files. An alternative to label argument.', ), 'examples' => array( 'drush config-export --destination' => 'Export configuration; Save files in a backup directory named config-export.', ), ); $items['config-import'] = array( 'description' => 'Import config from a config directory.', 'arguments' => array( 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'sync'", ), 'options' => array( 'preview' => array( 'description' => 'Format for displaying proposed changes. Recognized values: list, diff. Defaults to list.', 'example-value' => 'list', ), 'source' => array( 'description' => 'An arbitrary directory that holds the configuration files. An alternative to label argument', ), 'partial' => array( 'description' => 'Allows for partial config imports from the source directory. Only updates and new configs will be processed with this flag (missing configs will not be deleted).', ), ), 'core' => array('8+'), 'examples' => array( 'drush config-import --partial' => 'Import configuration; do not remove missing configuration.', ), 'aliases' => array('cim'), ); $items['config-list'] = array( 'description' => 'List config names by prefix.', 'core' => array('8+'), 'aliases' => array('cli'), 'arguments' => array( 'prefix' => 'The config prefix. For example, "system". No prefix will return all names in the system.', ), 'examples' => array( 'drush config-list system' => 'Return a list of all system config names.', 'drush config-list "image.style"' => 'Return a list of all image styles.', 'drush config-list --format="json"' => 'Return all config names as json.', ), 'outputformat' => array( 'default' => 'list', 'pipe-format' => 'var_export', 'output-data-type' => 'format-list', ), ); $items['config-edit'] = $deps + array( 'description' => 'Open a config file in a text editor. Edits are imported into active configuration after closing editor.', 'core' => array('8+'), 'aliases' => array('cedit'), 'arguments' => array( 'config-name' => 'The config object name, for example "system.site".', ), 'global-options' => array('editor', 'bg'), 'allow-additional-options' => array('config-import'), 'examples' => array( 'drush config-edit image.style.large' => 'Edit the image style configurations.', 'drush config-edit' => 'Choose a config file to edit.', 'drush config-edit --choice=2' => 'Edit the second file in the choice list.', 'drush --bg config-edit image.style.large' => 'Return to shell prompt as soon as the editor window opens.', ), ); $items['config-delete'] = array( 'description' => 'Delete a configuration object.', 'core' => array('8+'), 'aliases' => array('cdel'), 'arguments' => array( 'config-name' => 'The config object name, for example "system.site".', ), 'required arguments' ); $items['config-pull'] = array( 'description' => 'Export and transfer config from one environment to another.', // 'core' => array('8+'), Operates on remote sites so not possible to declare this locally. 'drush dependencies' => array('config', 'core'), // core-rsync, core-execute. 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'aliases' => array('cpull'), 'arguments' => array( 'source' => 'A site-alias or the name of a subdirectory within /sites whose config you want to copy from.', 'target' => 'A site-alias or the name of a subdirectory within /sites whose config you want to replace.', ), 'required-arguments' => TRUE, 'allow-additional-options' => array(), // Most options from config-export and core-rsync unusable. 'examples' => array( 'drush config-pull @prod @stage' => "Export config from @prod and transfer to @stage.", 'drush config-pull @prod @self --label=vcs' => "Export config from @prod and transfer to the 'vcs' config directory of current site.", ), 'options' => array( 'safe' => 'Validate that there are no git uncommitted changes before proceeding', 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'sync'", 'runner' => 'Where to run the rsync command; defaults to the local site. Can also be "source" or "destination".', ), 'topics' => array('docs-aliases', 'docs-config-exporting'), ); return $items; } /** * Implements hook_drush_help_alter(). */ function config_drush_help_alter(&$command) { // Hide additional-options which are for internal use only. if ($command['command'] == 'config-edit') { $command['options']['source']['hidden'] = TRUE; $command['options']['partial']['hidden'] = TRUE; } } /** * Config list command callback * * @param string $prefix * The config prefix to retrieve, or empty to return all. */ function drush_config_list($prefix = '') { $names = \Drupal::configFactory()->listAll($prefix); if (empty($names)) { // Just in case there is no config. if (!$prefix) { return drush_set_error(dt('No config storage names found.')); } else { return drush_set_error(dt('No config storage names found matching @prefix', array('@prefix' => $prefix))); } } return $names; } /** * Config get command callback. * * @param $config_name * The config name. * @param $key * The config key. */ function drush_config_get($config_name, $key = NULL) { if (!isset($key)) { return drush_config_get_object($config_name); } else { return drush_config_get_value($config_name, $key); } } /** * Config delete command callback. * * @param $config_name * The config name. */ function drush_config_delete($config_name) { $config =\Drupal::service('config.factory')->getEditable($config_name); if ($config->isNew()) { return drush_set_error('DRUSH_CONFIG_ERROR', 'Configuration name not recognized. Use config-list to see all names.'); } else { $config->delete(); } } /** * Config set command callback. * * @param $config_name * The config name. * @param $key * The config key. * @param $data * The data to save to config. */ function drush_config_set($config_name, $key = NULL, $data = NULL) { // This hidden option is a convenient way to pass a value without passing a key. $data = drush_get_option('value', $data); if (!isset($data)) { return drush_set_error('DRUSH_CONFIG_ERROR', dt('No config value specified.')); } $config = \Drupal::configFactory()->getEditable($config_name); // Check to see if config key already exists. if ($config->get($key) === NULL) { $new_key = TRUE; } else { $new_key = FALSE; } // Special flag indicating that the value has been passed via STDIN. if ($data === '-') { $data = stream_get_contents(STDIN); } // Now, we parse the value. switch (drush_get_option('format', 'string')) { case 'yaml': $parser = new Parser(); $data = $parser->parse($data, TRUE); } if (is_array($data) && drush_confirm(dt('Do you want to update or set multiple keys on !name config.', array('!name' => $config_name)))) { foreach ($data as $key => $value) { $config->set($key, $value); } return $config->save(); } else { $confirmed = FALSE; if ($config->isNew() && drush_confirm(dt('!name config does not exist. Do you want to create a new config object?', array('!name' => $config_name)))) { $confirmed = TRUE; } elseif ($new_key && drush_confirm(dt('!key key does not exist in !name config. Do you want to create a new config key?', array('!key' => $key, '!name' => $config_name)))) { $confirmed = TRUE; } elseif (drush_confirm(dt('Do you want to update !key key in !name config?', array('!key' => $key, '!name' => $config_name)))) { $confirmed = TRUE; } if ($confirmed && !drush_get_context('DRUSH_SIMULATE')) { return $config->set($key, $data)->save(); } } } /* * If provided $destination is not TRUE and not empty, make sure it is writable. */ function drush_config_export_validate() { $destination = drush_get_option('destination'); if ($destination === TRUE) { // We create a dir in command callback. No need to validate. return; } if (!empty($destination)) { $additional = array(); $values = drush_sitealias_evaluate_path($destination, $additional, TRUE); if (!isset($values['path'])) { return drush_set_error('config_export_target', 'The destination directory could not be evaluated.'); } $destination = $values['path']; drush_set_option('destination', $destination); if (!file_exists($destination)) { $parent = dirname($destination); if (!is_dir($parent)) { return drush_set_error('config_export_target', 'The destination parent directory does not exist.'); } if (!is_writable($parent)) { return drush_set_error('config_export_target', 'The destination parent directory is not writable.'); } } else { if (!is_dir($destination)) { return drush_set_error('config_export_target', 'The destination is not a directory.'); } if (!is_writable($destination)) { return drush_set_error('config_export_target', 'The destination directory is not writable.'); } } } } /** * Command callback: Export config to specified directory (usually sync). */ function drush_config_export($destination = NULL) { global $config_directories; // Determine which target directory to use. if ($target = drush_get_option('destination')) { if ($target === TRUE) { // User did not pass a specific value for --destination. Make one. /** @var drush_version_control_backup $backup */ $backup = drush_include_engine('version_control', 'backup'); $destination_dir = $backup->prepare_backup_dir('config-export'); } else { $destination_dir = $target; // It is important to be able to specify a destination directory that // does not exist yet, for exporting on remote systems drush_mkdir($destination_dir); } } else { $choices = drush_map_assoc(array_keys($config_directories)); unset($choices[CONFIG_ACTIVE_DIRECTORY]); if (!isset($destination) && count($choices) >= 2) { $destination = drush_choice($choices, 'Choose a destination.'); if (empty($destination)) { return drush_user_abort(); } } elseif (!isset($destination)) { $destination = CONFIG_SYNC_DIRECTORY; } $destination_dir = config_get_config_directory($destination); } // Prepare a new branch, if applicable $remote = drush_get_option('push', FALSE); $original_branch = FALSE; $branch = FALSE; if ($remote) { // Get the branch that we're on at the moment $result = drush_shell_cd_and_exec($destination_dir, 'git rev-parse --abbrev-ref HEAD'); if (!$result) { return drush_set_error('DRUSH_CONFIG_EXPORT_NO_GIT', dt("The drush config-export command requires that the selected configuration directory !dir be under git revision control when using --commit or --push options.", array('!dir' => $destination_dir))); } $output = drush_shell_exec_output(); $original_branch = $output[0]; $branch = drush_get_option('branch', FALSE); if (!$branch) { $branch = $original_branch; } if ($branch != $original_branch) { // Switch to the working branch; create it if it does not exist. // We do NOT want to use -B here, as we do NOT want to reset the // branch if it already exists. $result = drush_shell_cd_and_exec($destination_dir, 'git checkout %s', $branch); if (!$result) { $result = drush_shell_cd_and_exec($destination_dir, 'git checkout -b %s', $branch); } } } // Do the actual config export operation $result = _drush_config_export($destination, $destination_dir, $branch); // Regardless of the result of the export, reset to our original branch. if ($branch != $original_branch) { drush_shell_cd_and_exec($destination_dir, 'git checkout %s', $original_branch); } return $result; } function _drush_config_export($destination, $destination_dir, $branch) { $commit = drush_get_option('commit'); $comment = drush_get_option('message', 'Exported configuration.'); if (count(glob($destination_dir . '/*')) > 0) { // Retrieve a list of differences between the active and target configuration (if any). if ($destination == CONFIG_SYNC_DIRECTORY) { $target_storage = \Drupal::service('config.storage.sync'); } else { $target_storage = new FileStorage($destination_dir); } /** @var \Drupal\Core\Config\StorageInterface $active_storage */ $active_storage = \Drupal::service('config.storage'); $comparison_source = $active_storage; $config_comparer = new StorageComparer($comparison_source, $target_storage, \Drupal::service('config.manager')); if (!$config_comparer->createChangelist()->hasChanges()) { return drush_log(dt('The active configuration is identical to the configuration in the export directory (!target).', array('!target' => $destination_dir)), LogLevel::OK); } drush_print("Differences of the active config to the export directory:\n"); $change_list = array(); foreach ($config_comparer->getAllCollectionNames() as $collection) { $change_list[$collection] = $config_comparer->getChangelist(NULL, $collection); } // Print a table with changes in color, then re-generate again without // color to place in the commit comment. _drush_print_config_changes_table($change_list); $tbl = _drush_format_config_changes_table($change_list); $output = $tbl->getTable(); if (!stristr(PHP_OS, 'WIN')) { $output = str_replace("\r\n", PHP_EOL, $output); } $comment .= "\n\n$output"; if (!$commit && !drush_confirm(dt('The .yml files in your export directory (!target) will be deleted and replaced with the active config.', array('!target' => $destination_dir)))) { return drush_user_abort(); } // Only delete .yml files, and not .htaccess or .git. $target_storage->deleteAll(); } // Write all .yml files. $source_storage = \Drupal::service('config.storage'); if ($destination == CONFIG_SYNC_DIRECTORY) { $destination_storage = \Drupal::service('config.storage.sync'); } else { $destination_storage = new FileStorage($destination_dir); } foreach ($source_storage->listAll() as $name) { $destination_storage->write($name, $source_storage->read($name)); } // Export configuration collections. foreach (\Drupal::service('config.storage')->getAllCollectionNames() as $collection) { $source_storage = $source_storage->createCollection($collection); $destination_storage = $destination_storage->createCollection($collection); foreach ($source_storage->listAll() as $name) { $destination_storage->write($name, $source_storage->read($name)); } } drush_log(dt('Configuration successfully exported to !target.', array('!target' => $destination_dir)), LogLevel::SUCCESS); drush_backend_set_result($destination_dir); // Commit and push, or add exported configuration if requested. $remote = drush_get_option('push', FALSE); if ($commit || $remote) { // There must be changed files at the destination dir; if there are not, then // we will skip the commit-and-push step $result = drush_shell_cd_and_exec($destination_dir, 'git status --porcelain .'); if (!$result) { return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git status` failed.")); } $uncommitted_changes = drush_shell_exec_output(); if (!empty($uncommitted_changes)) { $result = drush_shell_cd_and_exec($destination_dir, 'git add -A .'); if (!$result) { return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git add -A` failed.")); } $comment_file = drush_save_data_to_temp_file($comment); $result = drush_shell_cd_and_exec($destination_dir, 'git commit --file=%s', $comment_file); if (!$result) { return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git commit` failed. Output:\n\n!output", array('!output' => implode("\n", drush_shell_exec_output())))); } if ($remote) { // Remote might be FALSE, if --push was not specified, or // it might be TRUE if --push was not given a value. if (!is_string($remote)) { $remote = 'origin'; } $result = drush_shell_cd_and_exec($destination_dir, 'git push --set-upstream %s %s', $remote, $branch); if (!$result) { return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git push` failed.")); } } } } elseif (drush_get_option('add')) { drush_shell_exec_interactive('git add -p %s', $destination_dir); } $values = array( 'destination' => $destination_dir, ); return $values; } function drush_config_import_validate() { drush_include_engine('drupal', 'environment'); if (drush_get_option('partial') && !drush_module_exists('config')) { return drush_set_error('config_import_partial', 'Enable the config module in order to use the --partial option.'); } if ($source = drush_get_option('source')) { if (!file_exists($source)) { return drush_set_error('config_import_target', 'The source directory does not exist.'); } if (!is_dir($source)) { return drush_set_error('config_import_target', 'The source is not a directory.'); } } } /** * Command callback. Import from specified config directory (defaults to sync). */ function drush_config_import($source = NULL) { global $config_directories; // Determine source directory. if ($target = drush_get_option('source')) { $source_dir = $target; } else { $choices = drush_map_assoc(array_keys($config_directories)); unset($choices[CONFIG_ACTIVE_DIRECTORY]); if (!isset($source) && count($choices) >= 2) { $source= drush_choice($choices, 'Choose a source.'); if (empty($source)) { return drush_user_abort(); } } elseif (!isset($source)) { $source = CONFIG_SYNC_DIRECTORY; } $source_dir = config_get_config_directory($source); } if ($source == CONFIG_SYNC_DIRECTORY) { $source_storage = \Drupal::service('config.storage.sync'); } else { $source_storage = new FileStorage($source_dir); } // Determine $source_storage in partial and non-partial cases. /** @var \Drupal\Core\Config\StorageInterface $active_storage */ $active_storage = \Drupal::service('config.storage'); if (drush_get_option('partial')) { $replacement_storage = new StorageReplaceDataWrapper($active_storage); foreach ($source_storage->listAll() as $name) { $data = $source_storage->read($name); $replacement_storage->replaceData($name, $data); } $source_storage = $replacement_storage; } /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */ $config_manager = \Drupal::service('config.manager'); $storage_comparer = new StorageComparer($source_storage, $active_storage, $config_manager); if (!$storage_comparer->createChangelist()->hasChanges()) { return drush_log(dt('There are no changes to import.'), LogLevel::OK); } if (drush_get_option('preview', 'list') == 'list') { $change_list = array(); foreach ($storage_comparer->getAllCollectionNames() as $collection) { $change_list[$collection] = $storage_comparer->getChangelist(NULL, $collection); } _drush_print_config_changes_table($change_list); } else { // Copy active storage to the temporary directory. $temp_dir = drush_tempdir(); $temp_storage = new FileStorage($temp_dir); $source_dir_storage = new FileStorage($source_dir); foreach ($source_dir_storage->listAll() as $name) { if ($data = $active_storage->read($name)) { $temp_storage->write($name, $data); } } drush_shell_exec('diff -x %s -u %s %s', '*.git', $temp_dir, $source_dir); $output = drush_shell_exec_output(); drush_print(implode("\n", $output)); } if (drush_confirm(dt('Import the listed configuration changes?'))) { return drush_op('_drush_config_import', $storage_comparer); } } // Copied from submitForm() at /core/modules/config/src/Form/ConfigSync.php function _drush_config_import(StorageComparer $storage_comparer) { $config_importer = new ConfigImporter( $storage_comparer, \Drupal::service('event_dispatcher'), \Drupal::service('config.manager'), \Drupal::lock(), \Drupal::service('config.typed'), \Drupal::moduleHandler(), \Drupal::service('module_installer'), \Drupal::service('theme_handler'), \Drupal::service('string_translation') ); if ($config_importer->alreadyImporting()) { drush_log('Another request may be synchronizing configuration already.', LogLevel::WARNING); } else{ try { // This is the contents of \Drupal\Core\Config\ConfigImporter::import. // Copied here so we can log progress. if ($config_importer->hasUnprocessedConfigurationChanges()) { $sync_steps = $config_importer->initialize(); foreach ($sync_steps as $step) { $context = array(); do { $config_importer->doSyncStep($step, $context); if (isset($context['message'])) { drush_log(str_replace('Synchronizing', 'Synchronized', (string)$context['message']), LogLevel::OK); } } while ($context['finished'] < 1); } } if ($config_importer->getErrors()) { throw new \Drupal\Core\Config\ConfigException('Errors occurred during import'); } else { drush_log('The configuration was imported successfully.', LogLevel::SUCCESS); } } catch (ConfigException $e) { // Return a negative result for UI purposes. We do not differentiate // between an actual synchronization error and a failed lock, because // concurrent synchronizations are an edge-case happening only when // multiple developers or site builders attempt to do it without // coordinating. $message = 'The import failed due for the following reasons:' . "\n"; $message .= implode("\n", $config_importer->getErrors()); watchdog_exception('config_import', $e); return drush_set_error('config_import_fail', $message); } } } /** * Edit command callback. */ function drush_config_edit($config_name = '') { // Identify and validate input. if ($config_name) { $config = \Drupal::configFactory()->get($config_name); if ($config->isNew()) { return drush_set_error(dt('Config !name does not exist', array('!name' => $config_name))); } } else { $config_names = \Drupal::configFactory()->listAll(); $choice = drush_choice($config_names, 'Choose a configuration.'); if (empty($choice)) { return drush_user_abort(); } else { $config_name = $config_names[$choice]; $config = \Drupal::configFactory()->get($config_name); } } $active_storage = $config->getStorage(); $contents = $active_storage->read($config_name); // Write tmp YAML file for editing $temp_dir = drush_tempdir(); $temp_storage = new FileStorage($temp_dir); $temp_storage->write($config_name, $contents); $exec = drush_get_editor(); drush_shell_exec_interactive($exec, $temp_storage->getFilePath($config_name)); // Perform import operation if user did not immediately exit editor. if (!drush_get_option('bg', FALSE)) { $options = drush_redispatch_get_options() + array('partial' => TRUE, 'source' => $temp_dir); $backend_options = array('interactive' => TRUE); return (bool) drush_invoke_process('@self', 'config-import', array(), $options, $backend_options); } } /** * Config pull validate callback * */ function drush_config_pull_validate($source, $destination) { if (drush_get_option('safe')) { $return = drush_invoke_process($destination, 'core-execute', array('git diff --quiet'), array('escape' => 0)); if ($return['error_status']) { return drush_set_error('DRUSH_GIT_DIRTY', 'There are uncommitted changes in your git working copy.'); } } } /** * Config pull command callback * * @param string $label * The config label which receives the transferred files. */ function drush_config_pull($source, $destination) { // @todo drush_redispatch_get_options() assumes you will execute same command. Not good. $global_options = drush_redispatch_get_options() + array( 'strict' => 0, ); // @todo If either call is made interactive, we don't get an $return['object'] back. $backend_options = array('interactive' => FALSE); if (drush_get_context('DRUSH_SIMULATE')) { $backend_options['backend-simulate'] = TRUE; } $export_options = array( // Use the standard backup directory on Destination. 'destination' => TRUE, ); drush_log(dt('Starting to export configuration on Target.'), LogLevel::OK); $return = drush_invoke_process($source, 'config-export', array(), $global_options + $export_options, $backend_options); if ($return['error_status']) { return drush_set_error('DRUSH_CONFIG_PULL_EXPORT_FAILED', dt('Config-export failed.')); } else { // Trailing slash assures that transfer files and not the containing dir. $export_path = $return['object'] . '/'; } $rsync_options = array( 'remove-source-files' => TRUE, 'delete' => TRUE, 'exclude-paths' => '.htaccess', 'yes' => TRUE, // No need to prompt as destination is always the target config directory. ); $label = drush_get_option('label', 'sync'); $runner = drush_get_runner($source, $destination, drush_get_option('runner', FALSE)); drush_log(dt('Starting to rsync configuration files from !source to !dest.', array('!source' => $source, '!dest' => $destination)), LogLevel::OK); // This comment applies similarly to sql-sync's use of core-rsync. // Since core-rsync is a strict-handling command and drush_invoke_process() puts options at end, we can't send along cli options to rsync. // Alternatively, add options like --ssh-options to a site alias (usually on the machine that initiates the sql-sync). $return = drush_invoke_process($runner, 'core-rsync', array("$source:$export_path", "$destination:%config-$label"), $rsync_options); if ($return['error_status']) { return drush_set_error('DRUSH_CONFIG_PULL_RSYNC_FAILED', dt('Config-pull rsync failed.')); } drush_backend_set_result($return['object']); } /** * Show and return a config object * * @param $config_name * The config object name. */ function drush_config_get_object($config_name) { $source = drush_get_option('source', 'active'); $include_overridden = drush_get_option('include-overridden', FALSE); if ($include_overridden) { // Displaying overrides only applies to active storage. $config = \Drupal::config($config_name); $data = $config->get(); } elseif ($source == 'active') { $config = \Drupal::service('config.storage'); $data = $config->read($config_name); } elseif ($source == 'sync') { $config = \Drupal::service('config.storage.sync'); $data = $config->read($config_name); } else { return drush_set_error(dt('Unknown value !value for config source.', array('!value' => $source))); } if ($data === FALSE) { return drush_set_error(dt('Config !name does not exist in !source configuration.', array('!name' => $config_name, '!source' => $source))); } if (empty($data)) { drush_log(dt('Config !name exists but has no data.', array('!name' => $config_name)), LogLevel::NOTICE); return; } return $data; } /** * Show and return a value from config system. * * @param $config_name * The config name. * @param $key * The config key. */ function drush_config_get_value($config_name, $key) { $data = drush_config_get_object($config_name); $parts = explode('.', $key); if (count($parts) == 1) { $value = isset($data[$key]) ? $data[$key] : NULL; } else { $value = NestedArray::getValue($data, $parts, $key_exists); $value = $key_exists ? $value : NULL; } $returns[$config_name . ':' . $key] = $value; if ($value === NULL) { return drush_set_error('DRUSH_CONFIG_ERROR', dt('No matching key found in !name config.', array('!name' => $config_name))); } else { return $returns; } } /** * Print a table of config changes. * * @param array $config_changes * An array of changes keyed by collection. */ function _drush_format_config_changes_table(array $config_changes, $use_color = FALSE) { if (!$use_color) { $red = "%s"; $yellow = "%s"; $green = "%s"; } else { $red = "\033[31;40m\033[1m%s\033[0m"; $yellow = "\033[1;33;40m\033[1m%s\033[0m"; $green = "\033[1;32;40m\033[1m%s\033[0m"; } $rows = array(); $rows[] = array('Collection', 'Config', 'Operation'); foreach ($config_changes as $collection => $changes) { foreach ($changes as $change => $configs) { switch ($change) { case 'delete': $colour = $red; break; case 'update': $colour = $yellow; break; case 'create': $colour = $green; break; default: $colour = "%s"; break; } foreach($configs as $config) { $rows[] = array( $collection, $config, sprintf($colour, $change) ); } } } $tbl = _drush_format_table($rows); return $tbl; } /** * Print a table of config changes. * * @param array $config_changes * An array of changes keyed by collection. */ function _drush_print_config_changes_table(array $config_changes) { $tbl = _drush_format_config_changes_table($config_changes, !drush_get_context('DRUSH_NOCOLOR')); $output = $tbl->getTable(); if (!stristr(PHP_OS, 'WIN')) { $output = str_replace("\r\n", PHP_EOL, $output); } drush_print(rtrim($output)); return $tbl; } /** * Command argument complete callback. */ function config_config_get_complete() { return _drush_config_names_complete(); } /** * Command argument complete callback. */ function config_config_set_complete() { return _drush_config_names_complete(); } /** * Command argument complete callback. */ function config_config_view_complete() { return _drush_config_names_complete(); } /** * Command argument complete callback. */ function config_config_edit_complete() { return _drush_config_names_complete(); } /** * Command argument complete callback. */ function config_config_import_complete() { return _drush_config_directories_complete(); } /** * Command argument complete callback. */ function config_config_export_complete() { return _drush_config_directories_complete(); } /** * Command argument complete callback. */ function config_config_pull_complete() { return array('values' => array_keys(_drush_sitealias_all_list())); } /** * Helper function for command argument complete callback. * * @return * Array of available config directories. */ function _drush_config_directories_complete() { drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION); global $config_directories; return array('values' => array_keys($config_directories)); } /** * Helper function for command argument complete callback. * * @return * Array of available config names. */ function _drush_config_names_complete() { drush_bootstrap_max(); return array('values' => $storage = \Drupal::service('config.storage')->listAll()); } ' * * @param * A string with the help section (prepend with 'drush:') * * @return * A string with the help text for your command. */ function core_drush_help($section) { switch ($section) { case 'meta:core:title': return dt("Core Drush commands"); case 'drush:php-script': return dt("Runs the given php script(s) after a full Drupal bootstrap. A useful alternative to eval command when your php is lengthy or you can't be bothered to figure out bash quoting. If you plan to share a script with others, consider making a full drush command instead, since that's more self-documenting. Drush provides commandline options to the script via drush_get_option('option-name'), and commandline arguments can be accessed either via drush_get_arguments(), which returns all arguments in an array, or drush_shift(), which removes the next argument from the list and returns it."); case 'drush:rsync': return dt("Sync the entire drupal directory or a subdirectory to a using ssh. Excludes reserved files and directories for supported VCSs. Useful for pushing copies of your tree to a staging server, or retrieving a files directory from a remote site. Relative paths start from the Drupal root directory if a site alias is used; otherwise they start from the current working directory."); case 'error:DRUSH_DRUPAL_DB_ERROR': $message = dt("Drush was not able to start (bootstrap) the Drupal database.\n"); $message .= dt("Hint: This may occur when Drush is trying to:\n"); $message .= dt(" * bootstrap a site that has not been installed or does not have a configured database. In this case you can select another site with a working database setup by specifying the URI to use with the --uri parameter on the command line. See `drush topic docs-aliases` for details.\n"); $message .= dt(" * connect the database through a socket. The socket file may be wrong or the php-cli may have no access to it in a jailed shell. See http://drupal.org/node/1428638 for details.\n"); $message .= dt("\nDrush was attempting to connect to: \n!credentials\n", array('!credentials' => _core_site_credentials(12))); return $message; case 'error:DRUSH_DRUPAL_BOOTSTRAP_ERROR': $message = dt("Drush was not able to start (bootstrap) Drupal.\n"); $message .= dt("Hint: This error can only occur once the database connection has already been successfully initiated, therefore this error generally points to a site configuration issue, and not a problem connecting to the database.\n"); $message .= dt("\nDrush was attempting to connect to: \n!credentials\n", array('!credentials' => _core_site_credentials(12))); return $message; break; } } /** * Implements hook_drush_help_alter(). */ function core_drush_help_alter(&$command) { // Drupal 8+ only options. if (drush_drupal_major_version() < 8) { if ($command['commandfile'] == 'core' && $command['command'] == 'updatedb') { unset($command['options']['entity-updates']); } } } /** * Implementation of hook_drush_command(). * * In this hook, you specify which commands your * drush module makes available, what it does and * description. * * Notice how this structure closely resembles how * you define menu hooks. * * @return * An associative array describing your command(s). */ function core_drush_command() { $items = array(); $items['version'] = array( 'description' => 'Show drush version.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, // No bootstrap. 'options' => array( 'pipe' => 'Print just the version number, and nothing else.', ), 'outputformat' => array( 'default' => 'key-value', 'pipe-format' => 'string', 'label' => 'Drush Version', 'output-data-type' => 'format-single', ), ); $items['core-cron'] = array( 'description' => 'Run all cron hooks in all active modules for specified site.', 'aliases' => array('cron'), 'topics' => array('docs-cron'), ); $items['updatedb'] = array( 'description' => 'Apply any database updates required (as with running update.php).', 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_SITE, 'global-options' => array( 'cache-clear', ), 'options' => array( 'entity-updates' => 'Run automatic entity schema updates at the end of any update hooks. Defaults to --no-entity-updates.', ), 'aliases' => array('updb'), ); $items['entity-updates'] = array( 'description' => 'Apply pending entity schema updates.', 'aliases' => array('entup'), 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, 'core' => array('8+'), ); $items['twig-compile'] = array( 'description' => 'Compile all Twig template(s).', 'aliases' => array('twigc'), 'core' => array('8+'), ); $items['updatedb-status'] = array( 'description' => 'List any pending database updates.', 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'csv', 'field-labels' => array('module' => 'Module', 'update_id' => 'Update ID', 'description' => 'Description'), 'fields-default' => array('module', 'update_id', 'description'), 'output-data-type' => 'format-table', ), 'aliases' => array('updbst'), ); $items['core-config'] = array( 'description' => 'Edit drushrc, site alias, and Drupal settings.php files.', 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'arguments' => array( 'filter' => 'A substring for filtering the list of files. Omit this argument to choose from loaded files.', ), 'global-options' => array('editor', 'bg'), 'examples' => array( 'drush core-config' => 'Pick from a list of config/alias/settings files. Open selected in editor.', 'drush --bg core-config' => 'Return to shell prompt as soon as the editor window opens.', 'drush core-config etc' => 'Edit the global configuration file.', 'drush core-config demo.alia' => 'Edit a particular alias file.', 'drush core-config sett' => 'Edit settings.php for the current Drupal site.', 'drush core-config --choice=2' => 'Edit the second file in the choice list.', ), 'aliases' => array('conf', 'config'), ); $items['core-status'] = array( 'description' => 'Provides a birds-eye view of the current Drupal installation, if any.', 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'aliases' => array('status', 'st'), 'examples' => array( 'drush core-status version' => 'Show all status lines that contain version information.', 'drush core-status --pipe' => 'A list key=value items separated by line breaks.', 'drush core-status drush-version --pipe' => 'Emit just the drush version with no label.', 'drush core-status config-sync --pipe' => 'Emit just the sync Config directory with no label.', ), 'arguments' => array( 'item' => 'Optional. The status item line(s) to display.', ), 'options' => array( 'show-passwords' => 'Show database password. Defaults to --no-show-passwords.', 'full' => 'Show all file paths and drush aliases in the report, even if there are a lot.', 'project' => array( 'description' => 'One or more projects that should be added to the path list', 'example-value' => 'foo,bar', ), ), 'outputformat' => array( 'default' => 'key-value', 'pipe-format' => 'json', 'field-labels' => array('drupal-version' => 'Drupal version', 'uri' => 'Site URI', 'db-driver' => 'Database driver', 'db-hostname' => 'Database hostname', 'db-port' => 'Database port', 'db-username' => 'Database username', 'db-password' => 'Database password', 'db-name' => 'Database name', 'db-status' => 'Database', 'bootstrap' => 'Drupal bootstrap', 'user' => 'Drupal user', 'theme' => 'Default theme', 'admin-theme' => 'Administration theme', 'php-bin' => 'PHP executable', 'php-conf' => 'PHP configuration', 'php-os' => 'PHP OS', 'drush-script' => 'Drush script', 'drush-version' => 'Drush version', 'drush-temp' => 'Drush temp directory', 'drush-conf' => 'Drush configuration', 'drush-alias-files' => 'Drush alias files', 'install-profile' => 'Install profile', 'root' => 'Drupal root', 'drupal-settings-file' => 'Drupal Settings File', 'site-path' => 'Site path', 'root' => 'Drupal root', 'site' => 'Site path', 'themes' => 'Themes path', 'modules' => 'Modules path', 'files' => 'File directory path', 'private' => 'Private file directory path', 'temp' => 'Temporary file directory path', 'config-sync' => 'Sync config path', 'files-path' => 'File directory path', 'temp-path' => 'Temporary file directory path', '%paths' => 'Other paths'), 'formatted-filter' => '_drush_core_status_format_table_data', 'private-fields' => 'db-password', 'simplify-single' => TRUE, 'table-metadata' => array( 'list-separator' => ' ', ), 'output-data-type' => 'format-list', ), 'topics' => array('docs-readme'), ); $items['core-requirements'] = array( 'description' => 'Provides information about things that may be wrong in your Drupal installation, if any.', 'aliases' => array('status-report','rq'), 'examples' => array( 'drush core-requirements' => 'Show all status lines from the Status Report admin page.', 'drush core-requirements --severity=2' => 'Show only the red lines from the Status Report admin page.', 'drush core-requirements --pipe' => 'Print out a short report in JSON format, where severity 2=error, 1=warning, and 0/-1=OK', ), 'options' => array( 'severity' => array( 'description' => 'Only show status report messages with a severity greater than or equal to the specified value.', 'value' => 'required', 'example-value' => '3', ), 'ignore' => 'Comma-separated list of requirements to remove from output. Run with --pipe to see key values to use.', ), 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'json', 'field-labels' => array('title' => 'Title', 'severity' => 'Severity', 'sid' => 'SID', 'description' => 'Description', 'value' => 'Summary', 'reason' => 'Reason', 'weight' => 'Weight'), 'fields-default' => array('title', 'severity', 'description'), 'column-widths' => array('severity' => 8), 'concatenate-columns' => array('description' => array('value', 'description')), 'strip-tags' => TRUE, 'ini-item' => 'sid', 'key-value-item' => 'severity', 'list-metadata' => array( 'list-item' => 'severity', ), 'output-data-type' => 'format-table', ), ); $items['php-eval'] = array( 'description' => 'Evaluate arbitrary php code after bootstrapping Drupal (if available).', 'examples' => array( 'drush php-eval \'variable_set("hello", "world");\'' => 'Sets the hello variable using Drupal API.', 'drush php-eval \'$node = node_load(1); print $node->title;\'' => 'Loads node with nid 1 and then prints its title.', 'drush php-eval "file_unmanaged_copy(\'$HOME/Pictures/image.jpg\', \'public://image.jpg\');"' => 'Copies a file whose path is determined by an environment\'s variable. Note the use of double quotes so the variable $HOME gets replaced by its value.', 'drush php-eval "node_access_rebuild();"' => 'Rebuild node access permissions.', ), 'arguments' => array( 'code' => 'PHP code', ), 'required-arguments' => TRUE, 'allow-additional-options' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'aliases' => array('eval', 'ev'), 'outputformat' => array( 'default' => 'var_export', ), ); $items['php-script'] = array( 'description' => "Run php script(s).", 'examples' => array( 'drush php-script scratch' => 'Run scratch.php script. See commands/core directory.', 'drush php-script example --script-path=/path/to/scripts:/another/path' => 'Run script from specified paths', 'drush php-script' => 'List all available scripts.', '' => '', "#!/usr/bin/env drush\n "Execute php code with a full Drupal bootstrap directly from a shell script.", ), 'arguments' => array( 'filename' => 'Optional. The file you wish to execute (without extension). If omitted, list files ending in .php in the current working directory and specified script-path. Some might not be real drush scripts. Beware.', ), 'options' => array( 'script-path' => array( 'description' => "Additional paths to search for scripts, separated by : (Unix-based systems) or ; (Windows).", 'example-value' => '~/scripts', ), ), 'allow-additional-options' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'aliases' => array('scr'), 'topics' => array('docs-examplescript', 'docs-scripts'), ); $items['core-execute'] = array( 'description' => 'Execute a shell command. Usually used with a site alias.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, // No bootstrap. 'arguments' => array( 'command' => 'The shell command to be executed.', ), 'options' => array( 'escape' => 'Escape parameters before executing them with the shell. Default is escape; use --no-escape to disable.', ) + drush_shell_exec_proc_build_options(), 'required-arguments' => TRUE, 'allow-additional-options' => TRUE, 'handle-remote-commands' => TRUE, 'strict-option-handling' => TRUE, 'examples' => array( 'drush core-execute git pull origin rebase' => 'Retrieve latest code from git', ), 'aliases' => array('exec', 'execute'), 'topics' => array('docs-aliases'), ); $items['core-rsync'] = array( 'description' => 'Rsync the Drupal tree to/from another server using ssh.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, // No bootstrap. 'arguments' => array( 'source' => 'May be rsync path or site alias. See rsync documentation and example.aliases.drushrc.php.', 'destination' => 'May be rsync path or site alias. See rsync documentation and example.aliases.drushrc.php.', ), 'options' => array( 'mode' => 'The unary flags to pass to rsync; --mode=rultz implies rsync -rultz. Default is -akz.', 'exclude-conf' => 'Excludes settings.php from being rsynced. Default.', 'include-conf' => 'Allow settings.php to be rsynced. Default is to exclude settings.php.', 'include-vcs' => 'Include special version control directories (e.g. .svn). Default is to exclude vcs files.', 'exclude-files' => 'Exclude the files directory.', 'exclude-sites' => 'Exclude all directories in "sites/" except for "sites/all".', 'exclude-other-sites' => 'Exclude all directories in "sites/" except for "sites/all" and the site directory for the site being synced. Note: if the site directory is different between the source and destination, use --exclude-sites followed by "drush rsync @from:%site @to:%site"', 'exclude-paths' => 'List of paths to exclude, seperated by : (Unix-based systems) or ; (Windows).', 'include-paths' => 'List of paths to include, seperated by : (Unix-based systems) or ; (Windows).', '{rsync-option-name}' => "Replace {rsync-option-name} with the rsync option (or option='value') that you would like to pass through to rsync. Examples include --delete, --exclude=*.sql, --filter='merge /etc/rsync/default.rules', etc. See the rsync documentation for a complete explanation of all the rsync options and values.", 'rsync-version' => 'Set to the version of rsync you are using to signal Drush to go into backwards-compatibility mode when using very old versions of rsync. For example, --rsync-version=2.6.8 or earlier will cause Drush to avoid the --remove-source-files flag.', ), 'strict-option-handling' => TRUE, 'examples' => array( 'drush rsync @dev @stage' => 'Rsync Drupal root from Drush alias dev to the alias stage. Either or both may be remote.', 'drush rsync ./ @stage:%files/img' => 'Rsync all files in the current directory to the \'img\' directory in the file storage folder on the Drush alias stage.', 'drush -s rsync @dev @stage --exclude=*.sql --delete' => "Simulate Rsync Drupal root from the Drush alias dev to the alias stage (one of which must be local), excluding all files that match the filter '*.sql' and delete all files on the destination that are no longer on the source.", ), 'aliases' => array('rsync'), 'topics' => array('docs-aliases'), ); $items['drupal-directory'] = array( 'description' => 'Return the filesystem path for modules/themes and other key folders.', 'arguments' => array( 'target' => 'A module/theme name, or special names like root, files, private, or an alias : path alias string such as @alias:%files. Defaults to root.', ), 'options' => array( 'component' => "The portion of the evaluated path to return. Defaults to 'path'; 'name' returns the site alias of the target.", 'local-only' => "Reject any target that specifies a remote site.", ), 'examples' => array( 'cd `drush dd devel`' => 'Navigate into the devel module directory', 'cd `drush dd` ' => 'Navigate to the root of your Drupal site', 'cd `drush dd files`' => 'Navigate to the files directory.', 'drush dd @alias:%files' => 'Print the path to the files directory on the site @alias.', 'edit `drush dd devel`/devel.module' => "Open devel module in your editor (customize 'edit' for your editor)", ), 'aliases' => array('dd'), 'bootstrap' => DRUSH_BOOTSTRAP_NONE, ); $items['batch-process'] = array( 'description' => 'Process operations in the specified batch set', 'hidden' => TRUE, 'arguments' => array( 'batch-id' => 'The batch id that will be processed.', ), 'required-arguments' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_LOGIN, ); $items['updatedb-batch-process'] = array( 'description' => 'Perform update functions', 'hidden' => TRUE, 'arguments' => array( 'batch-id' => 'The batch id that will be processed', ), 'required-arguments' => TRUE, // Drupal 7 needs DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION, while Drupal 8 needs _FULL. // Therefore we bootstrap to _FULL in commands/core/drupal/update.inc. 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION, ); $items['core-global-options'] = array( 'description' => 'All global options', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'csv', 'field-labels' => array('label' => 'Label', 'description' => 'Description'), 'output-data-type' => 'format-table', ), ); $items['core-quick-drupal'] = array( 'description' => 'Download, install, serve and login to Drupal with minimal configuration and dependencies.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'aliases' => array('qd', 'cutie'), 'arguments' => array( 'site' => 'Short name for the site to be created - used as a directory name and as sqlite file name. Optional - if omitted timestamped "quick-drupal" directory will be used instead.', 'projects' => 'A list of projects to download into the new site. If projects contain extensions (modules or themes) with the same name they will be enabled by default. See --enable option to control this behaviour further.', ), 'examples' => array( 'drush qd' => 'Download and install stable release of Drupal into a timestamped directory, start server, and open the site logged in as admin.', 'drush qd --profile=minimal --cache --core=drupal-8.0.x --yes' => 'Fire up dev release of Drupal site with minimal install profile.', 'drush qd testsite devel --server=:8081/admin --browser=firefox --cache --yes' => 'Fire up stable release (using the cache) of Drupal site called "testsite", download and enable devel module, start a server on port 8081 and open /admin in firefox.', 'drush qd commercesite --core=commerce_kickstart --profile=commerce_kickstart --cache --yes --watchdog' => 'Download and install the "Commerce Kickstart" distribution/install profile, display watchdog messages on the server console.', 'drush qd --makefile=mysite.make' => 'Create and install a site from a makefile.', ), ); // Add in options/engines. drush_core_quick_drupal_options($items); // Add in topics for engines $items += drush_get_engine_topics(); return $items; } /** * Command argument complete callback. * * @return * Array of available profile names. */ function core_site_install_complete() { $max = drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_ROOT); if ($max >= DRUSH_BOOTSTRAP_DRUPAL_ROOT) { return array('values' => array_keys(drush_find_profiles(DRUPAL_ROOT))); } } /** * Command argument complete callback. * * @return * Array of available site aliases. */ function core_core_rsync_complete() { return array('values' => array_keys(_drush_sitealias_all_list())); } /** * @defgroup engines Engine types * @{ */ /** * Implementation of hook_drush_engine_type_info(). */ function core_drush_engine_type_info() { $info = array(); $info['drupal'] = array(); return $info; } /** * Implements hook_drush_engine_ENGINE_TYPE(). */ function core_drush_engine_drupal() { $engines = array( 'batch' => array(), 'update'=> array(), 'environment' => array(), 'site_install' => array(), 'pm' => array(), 'cache' => array(), 'image' => array(), ); return $engines; } /** * @} End of "Engine types". */ /** * Command handler. Apply pending entity schema updates. */ function drush_core_entity_updates() { if (drush_get_context('DRUSH_SIMULATE')) { drush_log(dt('entity-updates command does not support --simulate option.'), LogLevel::OK); } drush_include_engine('drupal', 'update'); if (entity_updates_main() === FALSE) { return FALSE; } drush_drupal_cache_clear_all(); drush_log(dt('Finished performing updates.'), LogLevel::OK); } /** * Command handler. Execute update.php code from drush. */ function drush_core_updatedb() { if (drush_get_context('DRUSH_SIMULATE')) { drush_log(dt('updatedb command does not support --simulate option.'), LogLevel::OK); return TRUE; } drush_include_engine('drupal', 'update'); $result = update_main(); if ($result === FALSE) { return FALSE; } elseif ($result > 0) { // Clear all caches in a new process. We just performed major surgery. drush_drupal_cache_clear_all(); drush_log(dt('Finished performing updates.'), LogLevel::OK); } } /** * Command handler. List pending DB updates. */ function drush_core_updatedb_status() { require_once DRUSH_DRUPAL_CORE . '/includes/install.inc'; drupal_load_updates(); drush_include_engine('drupal', 'update'); list($pending, $start) = updatedb_status(); if (empty($pending)) { drush_log(dt("No database updates required"), LogLevel::OK); } return $pending; } function _core_site_credentials($right_margin = 0) { // Leave some space on the right so that we can put the result into the // drush_log, which will again wordwrap the result. $original_columns = drush_get_context('DRUSH_COLUMNS', 80); drush_set_context('DRUSH_COLUMNS', $original_columns - $right_margin); $status_table = _core_site_status_table(); $metadata = drush_get_command_format_metadata('core-status'); $output = drush_format($status_table, $metadata, 'key-value'); drush_set_context('DRUSH_COLUMNS', $original_columns); return $output; } function _core_path_aliases($project = '') { $paths = array(); $site_wide = drush_drupal_sitewide_directory(); $boot = drush_get_bootstrap_object(); if ($drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT')) { $paths['%root'] = $drupal_root; if ($site_root = drush_get_context('DRUSH_DRUPAL_SITE_ROOT')) { $paths['%site'] = $site_root; if (is_dir($modules_path = $boot->conf_path() . '/modules')) { $paths['%modules'] = $modules_path; } else { $paths['%modules'] = ltrim($site_wide . '/modules', '/'); } if (is_dir($themes_path = $boot->conf_path() . '/themes')) { $paths['%themes'] = $themes_path; } else { $paths['%themes'] = ltrim($site_wide . '/themes', '/'); } if (drush_drupal_major_version() >= 8 && drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION)) { try { if (isset($GLOBALS['config_directories'])) { foreach ($GLOBALS['config_directories'] as $label => $unused) { $paths["%config-$label"] = config_get_config_directory($label); } } } catch (Exception $e) { // Nothing to do. } } if (drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { $paths['%files'] = drush_file_get_public(); if ($private_path = drush_file_get_private()) { $paths['%private'] = $private_path; } } if (function_exists('file_directory_temp')) { $paths['%temp'] = file_directory_temp(); } // If the 'project' parameter was specified, then search // for a project (or a few) and add its path to the path list if (!empty($project)) { drush_include_engine('drupal', 'environment'); $projects = array_merge(drush_get_modules(), drush_get_themes()); foreach(explode(',', $project) as $target) { if (array_key_exists($target, $projects)) { $paths['%' . $target] = $drupal_root . '/' . _drush_extension_get_path($projects[$target]); } } } } } // Add in all of the global paths from $options['path-aliases'] $paths = array_merge($paths, drush_get_option('path-aliases', array())); return $paths; } function _core_site_status_table($project = '') { $phase = drush_get_context('DRUSH_BOOTSTRAP_PHASE'); if ($drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT')) { $status_table['drupal-version'] = drush_drupal_version(); $boot_object = drush_get_bootstrap_object(); $conf_dir = $boot_object->conf_path(); $settings_file = "$conf_dir/settings.php"; $status_table['drupal-settings-file'] = file_exists($settings_file) ? $settings_file : ''; if ($site_root = drush_get_context('DRUSH_DRUPAL_SITE_ROOT')) { $status_table['uri'] = drush_get_context('DRUSH_URI'); try { $sql = drush_sql_get_class(); $db_spec = $sql->db_spec(); $status_table['db-driver'] = $db_spec['driver']; if (!empty($db_spec['unix_socket'])) { $status_table['db-socket'] = $db_spec['unix_socket']; } elseif (isset($db_spec['host'])) { $status_table['db-hostname'] = $db_spec['host']; } $status_table['db-username'] = isset($db_spec['username']) ? $db_spec['username'] : NULL; $status_table['db-password'] = isset($db_spec['password']) ? $db_spec['password'] : NULL; $status_table['db-name'] = isset($db_spec['database']) ? $db_spec['database'] : NULL; $status_table['db-port'] = isset($db_spec['port']) ? $db_spec['port'] : NULL; if ($phase > DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION) { $status_table['install-profile'] = $boot_object->get_profile(); if ($phase > DRUSH_BOOTSTRAP_DRUPAL_DATABASE) { $status_table['db-status'] = dt('Connected'); if ($phase > DRUSH_BOOTSTRAP_DRUPAL_FULL) { $status_table['bootstrap'] = dt('Successful'); if ($phase == DRUSH_BOOTSTRAP_DRUPAL_LOGIN) { $status_table['user'] = drush_user_get_class()->getCurrentUserAsSingle()->getUsername(); } } } } } catch (Exception $e) { // Don't worry be happy. } } if (drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { $status_table['theme'] = drush_theme_get_default(); $status_table['admin-theme'] = drush_theme_get_admin(); } } if ($php_bin = drush_get_option('php')) { $status_table['php-bin'] = $php_bin; } $status_table['php-os'] = PHP_OS; if ($php_ini_files = _drush_core_config_php_ini_files()) { $status_table['php-conf'] = $php_ini_files; } $status_table['drush-script'] = DRUSH_COMMAND; $status_table['drush-version'] = DRUSH_VERSION; $status_table['drush-temp'] = drush_find_tmp(); $status_table['drush-conf'] = drush_flatten_array(drush_get_context_options('context-path', '')); $alias_files = _drush_sitealias_find_alias_files(); $status_table['drush-alias-files'] = $alias_files; $paths = _core_path_aliases($project); if (!empty($paths)) { foreach ($paths as $target => $one_path) { $name = $target; if (substr($name,0,1) == '%') { $name = substr($name,1); } $status_table[$name] = $one_path; } } // Store the paths into the '%paths' index; this will be // used by other code, but will not be included in the output // of the drush status command. $status_table['%paths'] = $paths; return $status_table; } // Adjust the status output for any non-pipe output format function _drush_core_status_format_table_data($output, $metadata) { if (drush_get_option('full', FALSE) == FALSE) { // Hide any key that begins with a % foreach ($output as $key => $value) { if ($key[0] == '%') { unset($output[$key]); } } // Hide 'Modules path' and 'Themes path' as well unset($output['modules']); unset($output['themes']); // Shorten the list of alias files if there are too many if (isset($output['drush-alias-files']) && count($output['drush-alias-files']) > 24) { $msg = dt("\nThere are !count more alias files. Run with --full to see the full list.", array('!count' => count($output['drush-alias-files']) - 1)); $output['drush-alias-files'] = array($output['drush-alias-files'][0] , $msg); } if (isset($output['drupal-settings-file']) && empty($output['drupal-settings-file'])) { $output['drupal-settings-file'] = dt('MISSING'); } } return $output; } /** * Command callback. Runs all cron hooks. */ function drush_core_cron() { if (drush_drupal_major_version() < 8) { $result = drupal_cron_run(); } else { $result = \Drupal::service('cron')->run(); } if ($result) { drush_log(dt('Cron run successful.'), LogLevel::SUCCESS); } else { return drush_set_error('DRUSH_CRON_FAILED', dt('Cron run failed.')); } } /** * Command callback. Edit drushrc and alias files. */ function drush_core_config($filter = NULL) { $all = drush_core_config_load(); // Apply any filter that was supplied. if ($filter) { foreach ($all as $key => $file) { if (strpos($file, $filter) === FALSE) { unset($all[$key]); } } } $all = drush_map_assoc(array_values($all)); $exec = drush_get_editor(); if (count($all) == 1) { $filepath = current($all); } else { $choice = drush_choice($all, 'Enter a number to choose which file to edit.', '!key'); if (!$choice) { return drush_user_abort(); } $filepath = $all[$choice]; } return drush_shell_exec_interactive($exec, $filepath, $filepath); } /** * Command argument complete callback. * * @return * Array of available configuration files for editing. */ function core_core_config_complete() { return array('values' => drush_core_config_load(FALSE)); } function drush_core_config_load($headers = TRUE) { $php_header = $php = $rcs_header = $rcs = $aliases_header = $aliases = $drupal_header = $drupal = array(); $php = _drush_core_config_php_ini_files(); if (!empty($php)) { if ($headers) { $php_header = array('phpini' => '-- PHP ini files --'); } } $bash = _drush_core_config_bash_files(); if (!empty($bash)) { if ($headers) { $bash_header = array('bash' => '-- Bash files --'); } } drush_sitealias_load_all(); if ($rcs = drush_get_context_options('context-path', TRUE)) { if ($headers) { $rcs_header = array('drushrc' => '-- Drushrc --'); } } if ($aliases = drush_get_context('drush-alias-files')) { if ($headers) { $aliases_header = array('aliases' => '-- Aliases --'); } } if ($site_root = drush_get_context('DRUSH_DRUPAL_SITE_ROOT')) { $drupal[] = realpath($site_root . '/settings.php'); if (file_exists($site_root . '/settings.local.php')) { $drupal[] = realpath($site_root . '/settings.local.php'); } $drupal[] = realpath(DRUPAL_ROOT . '/.htaccess'); if ($headers) { $drupal_header = array('drupal' => '-- Drupal --'); } } return array_merge($php_header, $php, $bash_header, $bash, $rcs_header, $rcs, $aliases_header, $aliases, $drupal_header, $drupal); } function _drush_core_config_php_ini_files() { $ini_files = array(); $ini_files[] = php_ini_loaded_file(); if ($drush_ini = getenv('DRUSH_INI')) { if (file_exists($drush_ini)) { $ini_files[] = $drush_ini; } } foreach (array(DRUSH_BASE_PATH, '/etc/drush', drush_server_home() . '/.drush') as $ini_dir) { if (file_exists($ini_dir . "/drush.ini")) { $ini_files[] = realpath($ini_dir . "/drush.ini"); } } return array_unique($ini_files); } function _drush_core_config_bash_files() { $bash_files = array(); $home = drush_server_home(); if ($bashrc = drush_init_find_bashrc($home)) { $bash_files[] = $bashrc; } $prompt = $home . '/.drush/drush.prompt.sh'; if (file_exists($prompt)) { $bash_files[] = $prompt; } return $bash_files; } /** * Command callback. Provides information from the 'Status Reports' admin page. */ function drush_core_requirements() { include_once DRUSH_DRUPAL_CORE . '/includes/install.inc'; $severities = array( REQUIREMENT_INFO => t('Info'), REQUIREMENT_OK => t('OK'), REQUIREMENT_WARNING => t('Warning'), REQUIREMENT_ERROR => t('Error'), ); drupal_load_updates(); drush_include_engine('drupal', 'environment'); $requirements = drush_module_invoke_all('requirements', 'runtime'); // If a module uses "$requirements[] = " instead of // "$requirements['label'] = ", then build a label from // the title. foreach($requirements as $key => $info) { if (is_numeric($key)) { unset($requirements[$key]); $new_key = strtolower(str_replace(' ', '_', $info['title'])); $requirements[$new_key] = $info; } } $ignore_requirements = drush_get_option_list('ignore'); foreach ($ignore_requirements as $ignore) { unset($requirements[$ignore]); } ksort($requirements); $min_severity = drush_get_option('severity', -1); foreach($requirements as $key => $info) { $severity = array_key_exists('severity', $info) ? $info['severity'] : -1; $requirements[$key]['sid'] = $severity; $requirements[$key]['severity'] = $severities[$severity]; if ($severity < $min_severity) { unset($requirements[$key]); } if (isset($requirements[$key]['description'])) { $requirements[$key]['description'] = drush_render($requirements[$key]['description']); } } return $requirements; } /** * Command callback. Provides a birds-eye view of the current Drupal * installation. */ function drush_core_status() { $status_table = _core_site_status_table(drush_get_option('project','')); // If args are specified, filter out any entry that is not named // (in other words, only show lines named by one of the arg values) $args = func_get_args(); if (!empty($args)) { $field_list = $args; $metadata = drush_get_command_format_metadata('core-status'); foreach ($metadata['field-labels'] as $field_key => $field_label) { if (_drush_core_is_named_in_array($field_label, $args)) { $field_list[] = $field_key; } } foreach ($status_table as $key => $value) { if (!_drush_core_is_named_in_array($key, $field_list)) { unset($status_table[$key]); } } } return $status_table; } // Command callback. Show all global options. Exposed via topic command. function drush_core_global_options() { drush_print(dt('These options are applicable to most drush commands. Most options can be disabled by using --no-option (i.e. --no-debug to disable --debug.)')); drush_print(); $fake = drush_global_options_command(FALSE); return drush_format_help_section($fake, 'options'); } function _drush_core_is_named_in_array($key, $the_array) { $is_named = FALSE; $simplified_key = str_replace(array(' ', '_', '-'), array('', '', ''), $key); foreach ($the_array as $name) { if (stristr($simplified_key, str_replace(array(' ', '_', '-'), array('', '', ''), $name))) { $is_named = TRUE; } } return $is_named; } /** * Callback for core-quick-drupal command. */ function drush_core_quick_drupal() { $requests = FALSE; $make_projects = array(); $args = func_get_args(); $name = drush_get_option('use-name'); drush_set_option('backend', TRUE); drush_set_option('strict', FALSE); // We fail option validation because do so much internal drush_invoke(). $makefile = drush_get_option('makefile'); $root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); if (drush_get_option('use-existing', ($root != FALSE))) { if (!$root) { return drush_set_error('QUICK_DRUPAL_NO_ROOT_SPECIFIED', 'Must specify site with --root when using --use-existing.'); } // If a --db-url was not explicitly provided, and if there is already // a settings.php file provided, then use the db-url defined inside it. if (!drush_get_option('db-url', FALSE)) { $values = drush_invoke_process('@self', 'site-alias', array('@self'), array('with-db-url' => TRUE), array('integrate' => FALSE, 'override-simulated' => TRUE)); if (!empty($values['object']['self']['db-url'])) { drush_set_option('db-url', $values['object']['self']['db-url']); } } if (empty($name)) { $name = basename($root); } $base = dirname($root); } else { if (!empty($args) && empty($name)) { $name = array_shift($args); } if (empty($name)) { $name = 'quick-drupal-' . gmdate('YmdHis', $_SERVER['REQUEST_TIME']); } $root = drush_get_option('root', FALSE); $core = drush_get_option('core', 'drupal'); $project_rename = $core; if ($root) { $base = dirname($root); $project_rename = basename($root); } else { $base = getcwd() . '/' . $name; $root = $base . '/' . $core; } if (!empty($makefile)) { // Invoke 'drush make $makefile'. $result = drush_invoke_process('@none', 'make', array($makefile, $root), array('core-quick-drupal' => TRUE)); if ($result['error_status'] != 0) { return drush_set_error('DRUSH_QD_MAKE', 'Could not make; aborting.'); } $make_projects = array_diff(array_keys($result['object']['projects']), array('drupal')); } else { drush_mkdir($base); drush_set_option('destination', $base); drush_set_option('drupal-project-rename', $project_rename); if (drush_invoke('pm-download', array($core)) === FALSE) { return drush_set_error('QUICK_DRUPAL_CORE_DOWNLOAD_FAIL', 'Drupal core download/extract failed.'); } } } if (!drush_get_option('db-url', FALSE)) { drush_set_option('db-url', 'sqlite://sites/' . strtolower(drush_get_option('sites-subdir', 'default')) . "/files/$name.sqlite"); } // We have just created a site root where one did not exist before. // We therefore must manually reset DRUSH_SELECTED_DRUPAL_ROOT to // our new root, and force a bootstrap to DRUSH_BOOTSTRAP_DRUPAL_ROOT. drush_set_context('DRUSH_SELECTED_DRUPAL_ROOT', $root); if (!drush_bootstrap_to_phase(DRUSH_BOOTSTRAP_DRUPAL_ROOT)) { return drush_set_error('QUICK_DRUPAL_ROOT_LOCATE_FAIL', 'Unable to locate Drupal root directory.'); } if (!empty($args)) { $requests = pm_parse_arguments($args, FALSE); } if ($requests) { // Unset --destination, so that downloads go to the site directories. drush_unset_option('destination'); if (drush_invoke('pm-download', $requests) === FALSE) { return drush_set_error('QUICK_DRUPAL_PROJECT_DOWNLOAD_FAIL', 'Project download/extract failed.'); } } drush_invoke('site-install', array(drush_get_option('profile'))); // Log in with the admin user. // TODO: If site-install is given a sites-subdir other than 'default', // then it will bootstrap to DRUSH_BOOTSTRAP_DRUPAL_SITE get the installer // to recognize the desired site directory. This somehow interferes // with our desire to bootstrap to DRUSH_BOOTSTRAP_DRUPAL_LOGIN here. // We could do the last few steps in a new process if uri is not 'default'. drush_set_option('user', '1'); if (!drush_bootstrap_to_phase(DRUSH_BOOTSTRAP_DRUPAL_LOGIN)) { return drush_set_error('QUICK_DRUPAL_INSTALL_FAIL', 'Drupal core install failed.'); } $enable = array_merge(pm_parse_arguments(drush_get_option('enable', $requests)), $make_projects); if (!empty($enable)) { if (drush_invoke('pm-enable', $enable) === FALSE) { return drush_set_error('QUICK_DRUPAL_PROJECT_ENABLE_FAIL', 'Project enable failed.'); } } $server = drush_get_option('server', '/'); if ($server) { $server_uri = runserver_uri($server); _drush_core_qd_cache_uri($server_uri); } if (!drush_get_option('no-server', FALSE)) { if ($server) { // Current CLI user is also the web server user, which is for development // only. Hence we can safely make the site directory writable. This makes // it easier to delete and edit settings.php. $boot = drush_get_bootstrap_object(); @chmod($boot->conf_path(), 0700); drush_invoke_process(array('root' => $root, 'uri' => $server_uri), 'runserver', array($server)); } } else { drush_print(dt('Login URL: ') . drush_invoke('user-login')); } } // Write a drushrc.php to cache the server information for future Drush calls function _drush_core_qd_cache_uri($uri) { $server = $uri['host']; if (!empty($uri["port"])) { $server .= ':' . $uri["port"]; } $dir = DRUPAL_ROOT . '/drush'; $target_file = $dir . '/drushrc.php'; drush_log(dt("Caching 'uri' !uri in !target", array('!uri' => $server, '!target' => $target_file)), LogLevel::OK); $create_file = TRUE; if (file_exists($target_file)) { // Don't bother to ask with --use-existing; just // continue. if (drush_get_option('use-existing', FALSE)) { $create_file = FALSE; } else { $create_file = drush_confirm(dt('!target already exists. Overwrite?', array('!target' => $target_file))); } } $content = << 'Drupal core to download. Defaults to "drupal" (latest stable version).', 'use-existing' => 'Use an existing Drupal root, specified with --root. Overrides --core. Defaults to true when run from an existing site.', 'profile' => 'The install profile to use. Defaults to standard.', 'enable' => 'Specific extensions (modules or themes) to enable. By default, extensions with the same name as requested projects will be enabled automatically.', 'server' => 'Host IP address and port number to bind to and path to open in web browser (hyphen to clear a default path), all elements optional. See runserver examples for shorthand.', 'no-server' => 'Avoid starting runserver (and browser) for the created Drupal site.', 'browser' => 'Optional name of a browser to open site in. If omitted the OS default browser will be used. Set --no-browser to disable.', 'use-name' => array('hidden' => TRUE, 'description' => 'Overrides "name" argument.'), 'makefile' => array('description' => 'Makefile to use. Makefile must specify which version of Drupal core to build.', 'example-value' => 'mysite.make', 'value' => 'optional'), 'root' => array('description' => 'Path to Drupal root.', 'example-value' => '/path/to/root', 'value' => 'optional'), ); $pm = pm_drush_command(); foreach ($pm['pm-download']['options'] as $option => $description) { if (is_array($description)) { $description = $description['description']; } $options[$option] = 'Download option: ' . $description; } // Unset a few options that are not usable here, as we control them ourselves // or they are otherwise implied by the environment. unset($options['destination']); unset($options['drupal-project-rename']); unset($options['default-major']); unset($options['use-site-dir']); $si = site_install_drush_command(); foreach ($si['site-install']['options'] as $option => $description) { if (is_array($description)) { $description = $description['description']; } $options[$option] = 'Site install option: ' . $description; } unset($options['sites-subdir']); $runserver = runserver_drush_command(); foreach ($runserver['runserver']['options'] as $option => $description) { $options[$option] = 'Runserver option: ' . $description; } unset($options['user']); $items['core-quick-drupal']['options'] = $options; $items['core-quick-drupal']['engines'] = $pm['pm-download']['engines']; } /** * Command callback. Runs "naked" php scripts * and drush "shebang" scripts ("#!/usr/bin/env drush"). */ function drush_core_php_script() { $found = FALSE; $script = NULL; if ($args = func_get_args()) { $script = $args[0]; } if ($script == '-') { return eval(stream_get_contents(STDIN)); } elseif (file_exists($script)) { $found = $script; } else { // Array of paths to search for scripts $searchpath['DIR'] = dirname(__FILE__); $searchpath['cwd'] = drush_cwd(); // Additional script paths, specified by 'script-path' option if ($script_path = drush_get_option('script-path', FALSE)) { foreach (explode(PATH_SEPARATOR, $script_path) as $path) { $searchpath[] = $path; } } drush_log(dt('Searching for scripts in ') . implode(',', $searchpath), LogLevel::DEBUG); if (!isset($script)) { // List all available scripts. $all = array(); foreach($searchpath as $key => $path) { $recurse = !(($key == 'cwd') || ($path == '/')); $all = array_merge( $all , array_keys(drush_scan_directory($path, '/\.php$/', array('.', '..', 'CVS'), NULL, $recurse)) ); } drush_print(implode("\n", $all)); } else { // Execute the specified script. foreach($searchpath as $path) { $script_filename = $path . '/' . $script; if (file_exists($script_filename . '.php')) { $script_filename .= '.php'; } if (file_exists($script_filename)) { $found = $script_filename; break; } $all[] = $script_filename; } if (!$found) { return drush_set_error('DRUSH_TARGET_NOT_FOUND', dt('Unable to find any of the following: @files', array('@files' => implode(', ', $all)))); } } } if ($found) { // Set the DRUSH_SHIFT_SKIP to two; this will cause // drush_shift to skip the next two arguments the next // time it is called. This allows scripts to get all // arguments, including the 'php-script' and script // pathname, via drush_get_arguments(), or it can process // just the arguments that are relevant using drush_shift(). drush_set_context('DRUSH_SHIFT_SKIP', 2); if (_drush_core_eval_shebang_script($found) === FALSE) { return include($found); } } } function drush_core_php_eval($php) { return eval($php . ';'); } /** * Evaluate a script that begins with #!drush php-script */ function _drush_core_eval_shebang_script($script_filename) { $found = FALSE; $fp = fopen($script_filename, "r"); if ($fp !== FALSE) { $line = fgets($fp); if (_drush_is_drush_shebang_line($line)) { $first_script_line = ''; while ($line = fgets($fp)) { $line = trim($line); if ($line == ' $target))); } } /** * Called for `drush version` or `drush --version` */ function drush_core_version() { return DRUSH_VERSION; } /** * Command callback. Execute specified shell code. Often used by shell aliases * that start with !. */ function drush_core_execute() { $result = TRUE; $escape = drush_get_option('escape', TRUE); // Get all of the args and options that appear after the command name. $args = drush_get_original_cli_args_and_options(); if ($escape) { for ($x = 0; $x < count($args); $x++) { // escape all args except for command separators. if (!in_array($args[$x], array('&&', '||', ';'))) { $args[$x] = drush_escapeshellarg($args[$x]); } } } $cmd = implode(' ', $args); // If we selected a Drupal site, then cwd to the site root prior to exec $cwd = FALSE; if ($selected_root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT')) { if (is_dir($selected_root)) { $cwd = getcwd(); drush_op('chdir', $selected_root); } } if ($alias = drush_get_context('DRUSH_TARGET_SITE_ALIAS')) { $site = drush_sitealias_get_record($alias); if (!empty($site['site-list'])) { $sites = drush_sitealias_resolve_sitelist($site); foreach ($sites as $site_name => $site_spec) { $result = _drush_core_execute_cmd($site_spec, $cmd); if (!$result) { break; } } } else { $result = _drush_core_execute_cmd($site, $cmd); } } else { // Must be a local command. $result = (drush_shell_proc_open($cmd) == 0); } // Restore the cwd if we changed it if ($cwd) { drush_op('chdir', $selected_root); } if (!$result) { return drush_set_error('CORE_EXECUTE_FAILED', dt("Command !command failed.", array('!command' => $cmd))); } return $result; } function drush_core_twig_compile() { require_once DRUSH_DRUPAL_CORE . "/themes/engines/twig/twig.engine"; // Scan all enabled modules and themes. // @todo refactor since \Drush\Boot\DrupalBoot::commandfile_searchpaths is similar. $ignored_modules = drush_get_option_list('ignored-modules', array()); $cid = drush_cid_install_profile(); if ($cached = drush_cache_get($cid)) { $ignored_modules[] = $cached->data; } foreach (array_diff(drush_module_list(), $ignored_modules) as $module) { $searchpaths[] = drupal_get_path('module', $module); } $themes = drush_theme_list(); foreach ($themes as $name => $theme) { $searchpaths[] = $theme->getPath(); } foreach ($searchpaths as $searchpath) { foreach ($file = drush_scan_directory($searchpath, '/\.html.twig/', array('tests')) as $file) { $relative = str_replace(drush_get_context('DRUSH_DRUPAL_ROOT'). '/', '', $file->filename); // @todo Dynamically disable twig debugging since there is no good info there anyway. twig_render_template($relative, array('theme_hook_original' => '')); drush_log(dt('Compiled twig template !path', array('!path' => $relative)), LogLevel::NOTICE); } } } /** * Helper function for drush_core_execute: run one shell command */ function _drush_core_execute_cmd($site, $cmd) { if (!empty($site['remote-host'])) { // Remote, so execute an ssh command with a bash fragment at the end. $exec = drush_shell_proc_build($site, $cmd, TRUE); return (drush_shell_proc_open($exec) == 0); } elseif (!empty($site['root']) && is_dir($site['root'])) { return (drush_shell_proc_open('cd ' . drush_escapeshellarg($site['root']) . ' && ' . $cmd) == 0); } return (drush_shell_proc_open($cmd) == 0); } TRUE to indicate the command is a topic (REQUIRED) // Begin the topic name with the name of the commandfile (just like // any other command). // $items['docs-readme'] = array( 'description' => 'README.md', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/README.md'), ); $items['docs-bisect'] = array( 'description' => 'git bisect and Drush may be used together to find the commit an error was introduced in.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/git-bisect.example.sh'), ); $items['docs-bashrc'] = array( 'description' => 'Bashrc customization examples for Drush.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/example.bashrc'), ); $items['docs-configuration'] = array( 'description' => 'Configuration overview with examples from example.drushrc.php.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/example.drushrc.php'), ); $items['docs-config-exporting'] = array( 'description' => 'Drupal configuration export instructions, including customizing configuration by environment.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/docs/config-exporting.md'), ); $items['docs-aliases'] = array( 'description' => 'Site aliases overview on creating your own aliases for commonly used Drupal sites with examples from example.aliases.drushrc.php.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/example.aliases.drushrc.php'), ); $items['docs-ini-files'] = array( 'description' => 'php.ini or drush.ini configuration to set PHP values for use with Drush.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/example.drush.ini'), ); $items['docs-bastion'] = array( 'description' => 'Bastion server configuration: remotely operate on a Drupal sites behind a firewall.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/docs/bastion.md'), ); $items['docs-bootstrap'] = array( 'description' => 'Bootstrap explanation: how Drush starts up and prepares the Drupal environment for use with the command.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/docs/bootstrap.md'), ); $items['docs-cron'] = array( 'description' => 'Crontab instructions for running your Drupal cron tasks via `drush cron`.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/docs/cron.md'), ); $items['docs-scripts'] = array( 'description' => 'Shell script overview on writing simple sequences of Drush statements.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/docs/shellscripts.md'), ); $items['docs-shell-aliases'] = array( 'description' => 'Shell alias overview on creating your own aliases for commonly used Drush commands.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/docs/shellaliases.md'), ); $items['docs-commands'] = array( 'description' => 'Drush command instructions on creating your own Drush commands.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/docs/commands.md'), ); $items['docs-errorcodes'] = array( 'description' => 'Error code list containing all identifiers used with drush_set_error.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, ); $items['docs-api'] = array( 'description' => 'Drush API', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/drush.api.php'), ); $items['docs-context'] = array( 'description' => 'Contexts overview explaining how Drush manages command line options and configuration file settings.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/docs/context.md'), ); $items['docs-examplescript'] = array( 'description' => 'Example Drush script.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/helloworld.script'), ); $items['docs-examplecommand'] = array( 'description' => 'Example Drush command file.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/sandwich.drush.inc'), ); $items['docs-example-sync-extension'] = array( 'description' => 'Example Drush commandfile that extends sql-sync to enable development modules in the post-sync hook.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/sync_enable.drush.inc'), ); $items['docs-example-sync-via-http'] = array( 'description' => 'Example Drush commandfile that extends sql-sync to allow transfer of the sql dump file via http rather than ssh and rsync.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/sync_via_http.drush.inc'), ); $items['docs-policy'] = array( 'description' => 'Example policy file.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/policy.drush.inc'), ); $items['docs-strict-options'] = array( 'description' => 'Strict option handling, and how commands that use it differ from regular Drush commands.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/docs/strict-options.md'), ); return $items; } /** * docs-errorcodes command. Print a list of all error codes * that can be found. */ function drush_docs_errorcodes() { $header = << $command) { $files = array_merge($files, drush_command_get_includes($command_name)); } // We will also search through all of the .inc files in the // drush includes directory $drush_include_files = drush_scan_directory(DRUSH_BASE_PATH . '/includes', '/.*\.inc$/', array('.', '..', 'CVS'), 0, FALSE); foreach ($drush_include_files as $filename => $info) { $files[$filename] = 'include'; } // Extract error messages from all command files $error_list = array(); foreach ($files as $file => $commandfile) { _drush_docs_find_set_error_calls($error_list, $file, $commandfile); } // Order error messages alphabetically by key ksort($error_list); // Convert to a table $data = array(); foreach ($error_list as $error_code => $error_messages) { $data[] = array($error_code, '-', implode("\n", $error_messages)); } $tmpfile = drush_tempnam('drush-errorcodes.'); file_put_contents($tmpfile, $header); $handle = fopen($tmpfile, 'a'); drush_print_table($data, FALSE, array(0 => 35), $handle); fclose($handle); drush_print_file($tmpfile); } /** * Search through a php source file looking for calls to * the function drush_set_error. If found, and if the * first parameter is an uppercase alphanumeric identifier, * then record the error code and the error message in our table. */ function _drush_docs_find_set_error_calls(&$error_list, $filename, $shortname) { $lines = file($filename); foreach ($lines as $line) { $matches = array(); // Find the error code after the drush_set_error call. The error code // should consist of uppercase letters and underscores only (numbers thrown in just in case) $match_result = preg_match("/.*drush_set_error[^'\"]['\"]([A-Z0-9_]*)['\"][^,]*,[^'\"]*(['\"])/", $line, $matches); if ($match_result) { $error_code = $matches[1]; $quote_char = $matches[2]; $error_message = ""; $message_start = strlen($matches[0]) - 1; // Regex adapted from http://stackoverflow.com/questions/1824325/regex-expression-for-escaped-quoted-string-wont-work-in-phps-preg-match-allif ($quote_char == '"') { if ($quote_char == '"') { $regex = '/"((?:[^\\\]*?(?:\\\")?)*?)"/'; } else { $regex = "/'((?:[^\\\]*?(?:\\\')?)*?)'/"; } $match_result = preg_match($regex, $line, $matches, 0, $message_start); if ($match_result) { $error_message = $matches[1]; } $error_list[$error_code] = array_key_exists($error_code, $error_list) ? array_merge($error_list[$error_code], array($error_message)) : array($error_message); } } } 0, ); $batch += $process_info; // The batch is now completely built. Allow other modules to make changes // to the batch so that it is easier to reuse batch processes in other // enviroments. if (drush_drupal_major_version() >= 8) { \Drupal::moduleHandler()->alter('batch', $batch); } else { drupal_alter('batch', $batch); } // Assign an arbitrary id: don't rely on a serial column in the 'batch' // table, since non-progressive batches skip database storage completely. $batch['id'] = db_next_id(); $args[] = $batch['id']; $batch['progressive'] = TRUE; // Move operations to a job queue. Non-progressive batches will use a // memory-based queue. foreach ($batch['sets'] as $key => $batch_set) { _batch_populate_queue($batch, $key); } drush_include_engine('drupal', 'environment'); // Store the batch. if (drush_drupal_major_version() >= 8) { /** @var \Drupal\Core\Batch\BatchStorage $batch_storage */ $batch_storage = \Drupal::service('batch.storage'); $batch_storage->create($batch); } else { db_insert('batch') ->fields(array( 'bid' => $batch['id'], 'timestamp' => REQUEST_TIME, 'token' => drush_get_token($batch['id']), 'batch' => serialize($batch), )) ->execute(); } $finished = FALSE; // Not used in D8+ and possibly earlier. global $user; while (!$finished) { $data = drush_invoke_process('@self', $command, $args, array('user' => drush_user_get_class()->getCurrentUserAsSingle()->id())); $finished = drush_get_error() || !$data || (isset($data['context']['drush_batch_process_finished']) && $data['context']['drush_batch_process_finished'] == TRUE); } } } /** * Initialize the batch command and call the worker function. * * Loads the batch record from the database and sets up the requirements * for the worker, such as registering the shutdown function. * * @param id * The batch id of the batch being processed. */ function _drush_batch_command($id) { $batch =& batch_get(); $data = db_query("SELECT batch FROM {batch} WHERE bid = :bid", array( ':bid' => $id))->fetchField(); if ($data) { $batch = unserialize($data); } else { return FALSE; } if (!isset($batch['running'])) { $batch['running'] = TRUE; } // Register database update for end of processing. register_shutdown_function('_drush_batch_shutdown'); if (_drush_batch_worker()) { _drush_batch_finished(); } } /** * Process batch operations * * Using the current $batch process each of the operations until the batch * has been completed or half of the available memory for the process has been * reached. */ function _drush_batch_worker() { $batch =& batch_get(); $current_set =& _batch_current_set(); $set_changed = TRUE; if (empty($current_set['start'])) { $current_set['start'] = microtime(TRUE); } $queue = _batch_queue($current_set); while (!$current_set['success']) { // If this is the first time we iterate this batch set in the current // request, we check if it requires an additional file for functions // definitions. if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) { include_once DRUPAL_ROOT . '/' . $current_set['file']; } $task_message = ''; // Assume a single pass operation and set the completion level to 1 by // default. $finished = 1; if ($item = $queue->claimItem()) { list($function, $args) = $item->data; // Build the 'context' array and execute the function call. $batch_context = array( 'sandbox' => &$current_set['sandbox'], 'results' => &$current_set['results'], 'finished' => &$finished, 'message' => &$task_message, ); // Magic wrap to catch changes to 'message' key. $batch_context = new DrushBatchContext($batch_context); // Tolerate recoverable errors. // See https://github.com/drush-ops/drush/issues/1930 $halt_on_error = drush_get_option('halt-on-error', TRUE); drush_set_option('halt-on-error', FALSE); call_user_func_array($function, array_merge($args, array(&$batch_context))); drush_set_option('halt-on-error', $halt_on_error); $finished = $batch_context['finished']; if ($finished >= 1) { // Make sure this step is not counted twice when computing $current. $finished = 0; // Remove the processed operation and clear the sandbox. $queue->deleteItem($item); $current_set['count']--; $current_set['sandbox'] = array(); } } // When all operations in the current batch set are completed, browse // through the remaining sets, marking them 'successfully processed' // along the way, until we find a set that contains operations. // _batch_next_set() executes form submit handlers stored in 'control' // sets (see form_execute_handlers()), which can in turn add new sets to // the batch. $set_changed = FALSE; $old_set = $current_set; while (empty($current_set['count']) && ($current_set['success'] = TRUE) && _batch_next_set()) { $current_set = &_batch_current_set(); $current_set['start'] = microtime(TRUE); $set_changed = TRUE; } // At this point, either $current_set contains operations that need to be // processed or all sets have been completed. $queue = _batch_queue($current_set); // If we are in progressive mode, break processing after 1 second. if (drush_memory_limit() > 0 && (memory_get_usage() * 2) >= drush_memory_limit()) { drush_log(dt("Batch process has consumed in excess of 50% of available memory. Starting new thread"), LogLevel::BATCH); // Record elapsed wall clock time. $current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2); break; } } // Reporting 100% progress will cause the whole batch to be considered // processed. If processing was paused right after moving to a new set, // we have to use the info from the new (unprocessed) set. if ($set_changed && isset($current_set['queue'])) { // Processing will continue with a fresh batch set. $remaining = $current_set['count']; $total = $current_set['total']; $progress_message = $current_set['init_message']; $task_message = ''; } else { // Processing will continue with the current batch set. $remaining = $old_set['count']; $total = $old_set['total']; $progress_message = $old_set['progress_message']; } $current = $total - $remaining + $finished; $percentage = _batch_api_percentage($total, $current); return ($percentage == 100); } /** * End the batch processing: * Call the 'finished' callbacks to allow custom handling of results, * and resolve page redirection. */ function _drush_batch_finished() { $batch = &batch_get(); // Execute the 'finished' callbacks for each batch set, if defined. foreach ($batch['sets'] as $batch_set) { if (isset($batch_set['finished'])) { // Check if the set requires an additional file for function definitions. if (isset($batch_set['file']) && is_file($batch_set['file'])) { include_once DRUPAL_ROOT . '/' . $batch_set['file']; } if (is_callable($batch_set['finished'])) { $queue = _batch_queue($batch_set); $operations = $queue->getAllItems(); $elapsed = $batch_set['elapsed'] / 1000; $elapsed = drush_drupal_major_version() >=8 ? \Drupal::service('date.formatter')->formatInterval($elapsed) : format_interval($elapsed); $batch_set['finished']($batch_set['success'], $batch_set['results'], $operations, $elapsed); } } } // Clean up the batch table and unset the static $batch variable. if (drush_drupal_major_version() >= 8) { /** @var \Drupal\Core\Batch\BatchStorage $batch_storage */ $batch_storage = \Drupal::service('batch.storage'); $batch_storage->delete($batch['id']); } else { db_delete('batch') ->condition('bid', $batch['id']) ->execute(); } foreach ($batch['sets'] as $batch_set) { if ($queue = _batch_queue($batch_set)) { $queue->deleteQueue(); } } $_batch = $batch; $batch = NULL; drush_set_option('drush_batch_process_finished', TRUE); } /** * Shutdown function: store the batch data for next request, * or clear the table if the batch is finished. */ function _drush_batch_shutdown() { if ($batch = batch_get()) { if (drush_drupal_major_version() >= 8) { /** @var \Drupal\Core\Batch\BatchStorage $batch_storage */ $batch_storage = \Drupal::service('batch.storage'); $batch_storage->update($batch); } else { db_update('batch') ->fields(array('batch' => serialize($batch))) ->condition('bid', $batch['id']) ->execute(); } } } 0, ); $batch += $process_info; // Initiate db storage in order to get a batch id. We have to provide // at least an empty string for the (not null) 'token' column. db_query("INSERT INTO {batch} (token, timestamp) VALUES ('', %d)", time()); $batch['id'] = db_last_insert_id('batch', 'bid'); $args[] = $batch['id']; // Actually store the batch data and the token generated form the batch id. db_query("UPDATE {batch} SET token = '%s', batch = '%s' WHERE bid = %d", drupal_get_token($batch['id']), serialize($batch), $batch['id']); $finished = FALSE; while (!$finished) { $data = drush_invoke_process('@self', $command, $args, $options); $finished = drush_get_error() || !$data || (isset($data['context']['drush_batch_process_finished']) && $data['context']['drush_batch_process_finished'] == TRUE); } } } /** * Initialize the batch command and call the worker function. * * Loads the batch record from the database and sets up the requirements * for the worker, such as registering the shutdown function. * * @param id * The batch id of the batch being processed. */ function _drush_batch_command($id) { $batch =& batch_get(); // Retrieve the current state of batch from db. if ($data = db_result(db_query("SELECT batch FROM {batch} WHERE bid = %d", $id))) { $batch = unserialize($data); } else { return FALSE; } if (!isset($batch['running'])) { $batch['running'] = TRUE; } // Register database update for end of processing. register_shutdown_function('_drush_batch_shutdown'); if (_drush_batch_worker()) { _drush_batch_finished(); } } /** * Process batch operations * * Using the current $batch process each of the operations until the batch * has been completed or half of the available memory for the process has been * reached. */ function _drush_batch_worker() { $batch =& batch_get(); $current_set =& _batch_current_set(); $set_changed = TRUE; timer_start('batch_processing'); while (!$current_set['success']) { // If this is the first time we iterate this batch set in the current // request, we check if it requires an additional file for functions // definitions. if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) { include_once($current_set['file']); } $finished = 1; $task_message = ''; if ((list($function, $args) = reset($current_set['operations'])) && function_exists($function)) { // Build the 'context' array, execute the function call, // and retrieve the user message. $batch_context = array('sandbox' => &$current_set['sandbox'], 'results' => &$current_set['results'], 'finished' => &$finished, 'message' => &$task_message); // Magic wrap to catch changes to 'message' key. $batch_context = new DrushBatchContext($batch_context); // Process the current operation. call_user_func_array($function, array_merge($args, array(&$batch_context))); $finished = $batch_context['finished']; } if ($finished >= 1) { // Make sure this step isn't counted double when computing $current. $finished = 0; // Remove the operation and clear the sandbox. array_shift($current_set['operations']); $current_set['sandbox'] = array(); } // If the batch set is completed, browse through the remaining sets, // executing 'control sets' (stored form submit handlers) along the way - // this might in turn insert new batch sets. // Stop when we find a set that actually has operations. $set_changed = FALSE; $old_set = $current_set; while (empty($current_set['operations']) && ($current_set['success'] = TRUE) && _batch_next_set()) { $current_set =& _batch_current_set(); $set_changed = TRUE; } // At this point, either $current_set is a 'real' batch set (has operations), // or all sets have been completed. // TODO - replace with memory check! // If we're in progressive mode, stop after 1 second. if ((memory_get_usage() * 2) >= drush_memory_limit()) { drush_log(dt("Batch process has consumed in excess of 50% of available memory. Starting new thread"), LogLevel::BATCH); break; } } // Gather progress information. // Reporting 100% progress will cause the whole batch to be considered // processed. If processing was paused right after moving to a new set, // we have to use the info from the new (unprocessed) one. if ($set_changed && isset($current_set['operations'])) { // Processing will continue with a fresh batch set. $remaining = count($current_set['operations']); $total = $current_set['total']; $task_message = ''; } else { $remaining = count($old_set['operations']); $total = $old_set['total']; } $current = $total - $remaining + $finished; $percentage = $total ? floor($current / $total * 100) : 100; return ($percentage == 100); } /** * End the batch processing: * Call the 'finished' callbacks to allow custom handling of results, * and resolve page redirection. */ function _drush_batch_finished() { $batch =& batch_get(); // Execute the 'finished' callbacks for each batch set. foreach ($batch['sets'] as $key => $batch_set) { if (isset($batch_set['finished'])) { // Check if the set requires an additional file for functions definitions. if (isset($batch_set['file']) && is_file($batch_set['file'])) { include_once($batch_set['file']); } if (function_exists($batch_set['finished'])) { $batch_set['finished']($batch_set['success'], $batch_set['results'], $batch_set['operations']); } } } // Cleanup the batch table and unset the global $batch variable. db_query("DELETE FROM {batch} WHERE bid = %d", $batch['id']); $_batch = $batch; $batch = NULL; drush_set_option('drush_batch_process_finished', TRUE); } /** * Shutdown function: store the batch data for next request, * or clear the table if the batch is finished. */ function _drush_batch_shutdown() { if ($batch = batch_get()) { db_query("UPDATE {batch} SET batch = '%s' WHERE bid = %d", serialize($batch), $batch['id']); } } 'drush_cache_clear_drush', 'all' => 'drush_cache_clear_both', ); if ($include_bootstrapped_types) { $types += array( 'theme-registry' => 'drush_cache_clear_theme_registry', 'menu' => 'menu_rebuild', 'css-js' => 'drush_cache_clear_css_js', 'block' => 'drush_cache_clear_block', 'module-list' => 'drush_get_modules', 'theme-list' => 'drush_get_themes', ); } $drupal_version = drush_drupal_major_version(); if ($drupal_version >= 7) { $types['registry'] = 'registry_update'; } elseif ($drupal_version == 6 && function_exists('module_exists') && module_exists('autoload')) { // TODO: move this to autoload module. $types['registry'] = 'autoload_registry_update'; } return $types; } function drush_cache_clear_theme_registry() { if (drush_drupal_major_version() >= 7) { drupal_theme_rebuild(); } else { cache_clear_all('theme_registry', 'cache', TRUE); } } function drush_cache_clear_menu() { return menu_router_rebuild(); } function drush_cache_clear_css_js() { _drupal_flush_css_js(); drupal_clear_css_cache(); drupal_clear_js_cache(); } /** * Clear the cache of the block output. */ function drush_cache_clear_block() { cache_clear_all(NULL, 'cache_block'); } /** * Clear caches internal to Drush core and Drupal. */ function drush_cache_clear_both() { drush_cache_clear_drush(); if (drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { drupal_flush_all_caches(); } } get($cid); } /** * The default bin. * * @return string */ function _drush_cache_bin_default() { return 'default'; } function _drush_cache_command_set($cid, $data, $bin, $expire, $tags) { if (is_null($bin)) { $bin = _drush_cache_bin_default(); } // Convert the "expire" argument to a valid value for Drupal's cache_set(). if ($expire == 'CACHE_TEMPORARY') { $expire = Cache::TEMPORARY; } if (!isset($expire) || $expire == 'CACHE_PERMANENT') { $expire = Cache::PERMANENT; } return \Drupal::cache($bin)->set($cid, $data, $expire, $tags); } function _drush_cache_clear_types($include_bootstrapped_types) { $types = array( 'drush' => 'drush_cache_clear_drush', ); if ($include_bootstrapped_types) { $types += array( 'theme-registry' => 'drush_cache_clear_theme_registry', 'router' => 'drush_cache_clear_router', 'css-js' => 'drush_cache_clear_css_js', 'module-list' => 'drush_get_modules', 'theme-list' => 'drush_get_themes', 'render' => 'drush_cache_clear_render', ); } return $types; } function drush_cache_clear_theme_registry() { \Drupal::service('theme.registry')->reset(); } function drush_cache_clear_router() { /** @var \Drupal\Core\Routing\RouteBuilderInterface $router_builder */ $router_builder = \Drupal::service('router.builder'); $router_builder->rebuild(); } function drush_cache_clear_css_js() { _drupal_flush_css_js(); drupal_clear_css_cache(); drupal_clear_js_cache(); } /** * Clear the cache of the block output. */ function drush_cache_clear_block() { // There is no distinct block cache in D8. See https://github.com/drush-ops/drush/issues/1531. // \Drupal::cache('block')->deleteAll(); } /** * Clears the render cache entries. */ function drush_cache_clear_render() { Cache::invalidateTags(['rendered']); } $module) { if ((!$include_hidden) && (!empty($module->info['hidden']))) { unset($modules[$key]); } else { $module->schema_version = drupal_get_installed_schema_version($key); } } return $modules; } /** * Returns drupal required modules, including modules declared as required dynamically. */ function _drush_drupal_required_modules($module_info) { $required = drupal_required_modules(); foreach ($module_info as $name => $module) { if (isset($module->info['required']) && $module->info['required']) { $required[] = $name; } } return array_unique($required); } /** * Return dependencies and its status for modules. * * @param $modules * Array of module names * @param $module_info * Drupal 'files' array for modules as returned by drush_get_modules(). * @return * Array with dependencies and status for $modules */ function drush_check_module_dependencies($modules, $module_info) { $status = array(); foreach ($modules as $key => $module) { $dependencies = array_reverse($module_info[$module]->requires); $unmet_dependencies = array_diff(array_keys($dependencies), array_keys($module_info)); if (!empty($unmet_dependencies)) { $status[$key]['error'] = array( 'code' => 'DRUSH_PM_ENABLE_DEPENDENCY_NOT_FOUND', 'message' => dt('Module !module cannot be enabled because it depends on the following modules which could not be found: !unmet_dependencies', array('!module' => $module, '!unmet_dependencies' => implode(',', $unmet_dependencies))) ); } else { // check for version incompatibility foreach ($dependencies as $dependency_name => $v) { $current_version = $module_info[$dependency_name]->info['version']; $current_version = str_replace(drush_get_drupal_core_compatibility() . '-', '', $current_version); $incompatibility = drupal_check_incompatibility($v, $current_version); if (isset($incompatibility)) { $status[$key]['error'] = array( 'code' => 'DRUSH_PM_ENABLE_DEPENDENCY_VERSION_MISMATCH', 'message' => dt('Module !module cannot be enabled because it depends on !dependency !required_version but !current_version is available', array('!module' => $module, '!dependency' => $dependency_name, '!required_version' => $incompatibility, '!current_version' => $current_version)) ); } } } $status[$key]['unmet-dependencies'] = $unmet_dependencies; $status[$key]['dependencies'] = $dependencies; } return $status; } /** * Return dependents of modules. * * @param $modules * Array of module names * @param $module_info * Drupal 'files' array for modules as returned by drush_get_modules(). * @return * Array with dependents for each one of $modules */ function drush_module_dependents($modules, $module_info) { $dependents = array(); foreach ($modules as $module) { $keys = array_keys($module_info[$module]->required_by); $dependents = array_merge($dependents, array_combine($keys, $keys)); } return array_unique($dependents); } /** * Returns a list of enabled modules. * * This is a wrapper for module_list(). */ function drush_module_list() { $modules = array_keys(\Drupal::moduleHandler()->getModuleList()); return array_combine($modules, $modules); } /** * Installs a given list of modules. * * @see \Drupal\Core\Extension\ModuleInstallerInterface::install() * */ function drush_module_install($module_list, $enable_dependencies = TRUE) { return \Drupal::service('module_installer')->install($module_list, $enable_dependencies); } /** * Checks that a given module exists and is enabled. * * @see \Drupal\Core\Extension\ModuleHandlerInterface::moduleExists() * */ function drush_module_exists($module) { return \Drupal::moduleHandler()->moduleExists($module); } /** * Determines which modules are implementing a hook. * * @param string $hook * The hook name. * @param bool $sort * Not used in Drupal 8 environment. * @param bool $reset * TRUE to reset the hook implementation cache. * * @see \Drupal\Core\Extension\ModuleHandlerInterface::getImplementations(). * @see \Drupal\Core\Extension\ModuleHandlerInterface::resetImplementations(). * */ function drush_module_implements($hook, $sort = FALSE, $reset = FALSE) { // $sort is there for consistency, but looks like Drupal 8 has no equilavient for it. // We can sort the list manually later if really needed. if ($reset == TRUE){ \Drupal::moduleHandler()->resetImplementations(); } return \Drupal::moduleHandler()->getImplementations($hook); } /** * Return a list of modules from a list of named modules. * Both enabled and disabled/uninstalled modules are returned. */ function drush_get_named_extensions_list($extensions) { $result = array(); $modules = drush_get_modules(); foreach($modules as $name => $module) { if (in_array($name, $extensions)) { $result[$name] = $module; } } $themes = drush_get_themes(); foreach($themes as $name => $theme) { if (in_array($name, $extensions)) { $result[$name] = $theme; } } return $result; } /** * Enable a list of modules. It is assumed the list contains all the dependencies not already enabled. * * @param $modules * Array of module names */ function drush_module_enable($modules) { // The list of modules already have all the dependencies, but they might not // be in the correct order. Still pass $enable_dependencies = TRUE so that // Drupal will enable the modules in the correct order. drush_module_install($modules); // Our logger got blown away during the container rebuild above. $boot = drush_select_bootstrap_class(); $boot->add_logger(); // Flush all caches. No longer needed in D8 per https://github.com/drush-ops/drush/issues/1207 // drupal_flush_all_caches(); } /** * Disable a list of modules. It is assumed the list contains all dependents not already disabled. * * @param $modules * Array of module names */ function drush_module_disable($modules) { drush_set_error('DRUSH_MODULE_DISABLE', dt('Drupal 8 does not support disabling modules. Use pm-uninstall instead.')); } /** * Uninstall a list of modules. * * @param $modules * Array of module names * * @see \Drupal\Core\Extension\ModuleInstallerInterface::uninstall() */ function drush_module_uninstall($modules) { \Drupal::service('module_installer')->uninstall($modules); // Our logger got blown away during the container rebuild above. $boot = drush_select_bootstrap_class(); $boot->add_logger(); } /** * Invokes a hook in a particular module. * */ function drush_module_invoke($module, $hook) { $args = func_get_args(); // Remove $module and $hook from the arguments. unset($args[0], $args[1]); return \Drupal::moduleHandler()->invoke($module, $hook, $args); } /** * Invokes a hook in all enabled modules that implement it. * */ function drush_module_invoke_all($hook) { $args = func_get_args(); // Remove $hook from the arguments. array_shift($args); return \Drupal::moduleHandler()->invokeAll($hook, $args); } /** * Returns a list of enabled themes. Use drush_get_themes() if you need to rebuild * and include hidden as well. * * @return \Drupal\Core\Extension\Extension[] * A list of themes keyed by name. */ function drush_theme_list() { $theme_handler = \Drupal::service('theme_handler'); return $theme_handler->listInfo(); } /** * Get complete information for all available themes. * * @param $include_hidden * Boolean to indicate whether hidden themes should be excluded or not. * @return * An array containing theme info for all available themes. */ function drush_get_themes($include_hidden = TRUE) { $themes = \Drupal::service('theme_handler')->rebuildThemeData(); foreach ($themes as $key => $theme) { if (!$include_hidden) { if (isset($theme->info['hidden'])) { // Don't exclude default or admin theme. if ($key != _drush_theme_default() && $key != _drush_theme_admin()) { unset($themes[$key]); } } } } return $themes; } /** * Enable a list of themes. * * @param $themes * Array of theme names. */ function drush_theme_enable($themes) { \Drupal::service('theme_handler')->install($themes); } /** * Disable a list of themes. * * @param $themes * Array of theme names. */ function drush_theme_disable($themes) { drush_set_error('DRUSH_THEME_DISABLE', dt('Drupal 8 does not support disabling themes. Use pm-uninstall instead.')); } /** * Uninstall a list of themes. * * @param $themes * Array of theme names * * @see \Drupal\Core\Extension\ThemeHandlerInterface::uninstall() */ function drush_theme_uninstall($themes) { \Drupal::service('theme_handler')->uninstall($themes); // Our logger got blown away during the container rebuild above. $boot = drush_select_bootstrap_class(); $boot->add_logger(); } /** * Helper function to obtain the severity levels based on Drupal version. * * @return array * Watchdog severity levels keyed by RFC 3164 severities. */ function drush_watchdog_severity_levels() { return array( RfcLogLevel::EMERGENCY => LogLevel::EMERGENCY, RfcLogLevel::ALERT => LogLevel::ALERT, RfcLogLevel::CRITICAL => LogLevel::CRITICAL, RfcLogLevel::ERROR => LogLevel::ERROR, RfcLogLevel::WARNING => LogLevel::WARNING, RfcLogLevel::NOTICE => LogLevel::NOTICE, RfcLogLevel::INFO => LogLevel::INFO, RfcLogLevel::DEBUG => LogLevel::DEBUG, ); } /** * Helper function to obtain the message types based on drupal version. * * @return * Array of watchdog message types. */ function drush_watchdog_message_types() { return _dblog_get_message_types(); } function _drush_theme_default() { return \Drupal::config('system.theme')->get('default'); } function _drush_theme_admin() { $theme = \Drupal::config('system.theme')->get('admin'); return empty($theme) ? 'seven' : $theme; } function _drush_file_public_path() { return PublicStream::basePath(); } function _drush_file_private_path() { return PrivateStream::basePath(); } /** * Gets the extension name. * * @param $info * The extension info. * @return string * The extension name. */ function _drush_extension_get_name($info) { return $info->getName(); } /** * Gets the extension type. * * @param $info * The extension info. * @return string * The extension type. */ function _drush_extension_get_type($info) { return $info->getType(); } /** * Gets the extension path. * * @param $info * The extension info. * @return string * The extension path. */ function _drush_extension_get_path($info) { return $info->getPath(); } /* * Wrapper for CSRF token generation. */ function drush_get_token($value = NULL) { return \Drupal::csrfToken()->get($value); } /* * Wrapper for _url(). */ function drush_url($path = NULL, array $options = array()) { return \Drupal\Core\Url::fromUserInput('/' . $path, $options)->toString(); } /** * Output a Drupal render array, object or string as plain text. * * @param string $data * Data to render. * * @return string * The plain-text representation of the input. */ function drush_render($data) { if (is_array($data)) { $data = \Drupal::service('renderer')->renderRoot($data); } $data = \Drupal\Core\Mail\MailFormatHelper::htmlToText($data); return $data; } $module) { if (!isset($module->type)) { $module->type = 'module'; } if ((!$include_hidden) && isset($module->info['hidden']) && ($module->info['hidden'])) { unset($modules[$key]); } } return $modules; } /** * Returns drupal required modules, including their dependencies. * * A module may alter other module's .info to set a dependency on it. * See for example http://drupal.org/project/phpass */ function _drush_drupal_required_modules($module_info) { $required = drupal_required_modules(); foreach ($required as $module) { $required = array_merge($required, $module_info[$module]->info['dependencies']); } return $required; } /** * Return dependencies and its status for modules. * * @param $modules * Array of module names * @param $module_info * Drupal 'files' array for modules as returned by drush_get_modules(). * @return * Array with dependencies and status for $modules */ function drush_check_module_dependencies($modules, $module_info) { $status = array(); foreach ($modules as $key => $module) { $dependencies = array_reverse($module_info[$module]->info['dependencies']); $unmet_dependencies = array_diff($dependencies, array_keys($module_info)); if (!empty($unmet_dependencies)) { $status[$key]['error'] = array( 'code' => 'DRUSH_PM_ENABLE_DEPENDENCY_NOT_FOUND', 'message' => dt('Module !module cannot be enabled because it depends on the following modules which could not be found: !unmet_dependencies', array('!module' => $module, '!unmet_dependencies' => implode(',', $unmet_dependencies))) ); } $status[$key]['unmet-dependencies'] = $unmet_dependencies; $status[$key]['dependencies'] = array(); foreach ($dependencies as $dependency) { $status[$key]['dependencies'][$dependency] = array('name' => $dependency); } } return $status; } /** * Return dependents of modules. * * @param $modules * Array of module names * @param $module_info * Drupal 'files' array for modules as returned by drush_get_modules(). * @return * Array with dependents for each one of $modules */ function drush_module_dependents($modules, $module_info) { $dependents = array(); foreach ($modules as $module) { $dependents = array_merge($dependents, $module_info[$module]->info['dependents']); } return array_unique($dependents); } /** * Returns a list of enabled modules. * * This is a simplified version of module_list(). */ function drush_module_list() { $enabled = array(); $rsc = drush_db_select('system', 'name', 'type=:type AND status=:status', array(':type' => 'module', ':status' => 1)); while ($row = drush_db_result($rsc)) { $enabled[$row] = $row; } return $enabled; } /** * Return a list of extensions from a list of named extensions. * Both enabled and disabled/uninstalled extensions are returned. */ function drush_get_named_extensions_list($extensions) { $result = array(); $rsc = drush_db_select('system', array('name', 'status'), 'name IN (:extensions)', array(':extensions' => $extensions)); while ($row = drush_db_fetch_object($rsc)) { $result[$row->name] = $row; } return $result; } /** * Enable a list of modules. It is assumed the list contains all the dependencies not already enabled. * * @param $modules * Array of module names */ function drush_module_enable($modules) { // Try to install modules previous to enabling. foreach ($modules as $module) { _drupal_install_module($module); } module_enable($modules); drush_system_modules_form_submit(); } /** * Disable a list of modules. It is assumed the list contains all dependents not already disabled. * * @param $modules * Array of module names */ function drush_module_disable($modules) { module_disable($modules, FALSE); drush_system_modules_form_submit(); } /** * Uninstall a list of modules. * * @param $modules * Array of module names */ function drush_module_uninstall($modules) { require_once DRUSH_DRUPAL_CORE . '/includes/install.inc'; foreach ($modules as $module) { drupal_uninstall_module($module); } } /** * Checks that a given module exists and is enabled. * * @see module_exists(). * */ function drush_module_exists($module) { return module_exists($module); } /** * Determines which modules are implementing a hook. * */ function drush_module_implements($hook, $sort = FALSE, $reset = FALSE) { return module_implements($hook, $sort, $reset); } /** * Invokes a hook in a particular module. * */ function drush_module_invoke($module, $hook) { $args = func_get_args(); return call_user_func_array('module_invoke', $args); } /** * Invokes a hook in all enabled modules that implement it. * */ function drush_module_invoke_all($hook) { $args = func_get_args(); return call_user_func_array('module_invoke_all', $args); } /** * Submit the system modules form. * * The modules should already be fully enabled/disabled before calling this * function. Calling this function just makes sure any activities triggered by * the form submit (such as admin_role) are completed. */ function drush_system_modules_form_submit() { $active_modules = array(); foreach (drush_get_modules(FALSE) as $key => $module) { if ($module->status == 1) { $active_modules[$key] = $key; } } module_load_include('inc', 'system', 'system.admin'); $form_state = array('values' => array('status' => $active_modules)); drupal_execute('system_modules', $form_state); } /** * Returns a list of enabled themes. Use drush_get_themes() if you need to rebuild * and include hidden/disabled as well. * * @return array * A list of themes keyed by name. */ function drush_theme_list() { $enabled = array(); foreach (list_themes() as $key => $info) { if ($info->status) { $enabled[$key] = $info; } } return $enabled; } /** * Get complete information for all available themes. * * We need to set the type for those themes that are not already in the system table. * * @param $include_hidden * Boolean to indicate whether hidden themes should be excluded or not. * @return * An array containing theme info for all available themes. */ function drush_get_themes($include_hidden = TRUE) { $themes = system_theme_data(); foreach ($themes as $key => $theme) { if (!isset($theme->type)) { $theme->type = 'theme'; } if ((!$include_hidden) && isset($theme->info['hidden']) && ($theme->info['hidden'])) { unset($themes[$key]); } } return $themes; } /** * Enable a list of themes. * * This function is based on system_themes_form_submit(). * * @see system_themes_form_submit() * @param $themes * Array of theme names. */ function drush_theme_enable($themes) { drupal_clear_css_cache(); foreach ($themes as $theme) { system_initialize_theme_blocks($theme); } db_query("UPDATE {system} SET status = 1 WHERE type = 'theme' AND name IN (".db_placeholders($themes, 'text').")", $themes); list_themes(TRUE); menu_rebuild(); module_invoke('locale', 'system_update', $themes); } /** * Disable a list of themes. * * This function is based on system_themes_form_submit(). * * @see system_themes_form_submit() * @param $themes * Array of theme names. */ function drush_theme_disable($themes) { drupal_clear_css_cache(); db_query("UPDATE {system} SET status = 0 WHERE type = 'theme' AND name IN (".db_placeholders($themes, 'text').")", $themes); list_themes(TRUE); menu_rebuild(); drupal_rebuild_theme_registry(); module_invoke('locale', 'system_update', $themes); } /** * Helper function to obtain the severity levels based on Drupal version. * * This is a copy of watchdog_severity_levels() without t(). * * Severity levels, as defined in RFC 3164: http://www.ietf.org/rfc/rfc3164.txt. * * @return * Array of watchdog severity levels. */ function drush_watchdog_severity_levels() { return array( WATCHDOG_EMERG => 'emergency', WATCHDOG_ALERT => 'alert', WATCHDOG_CRITICAL => 'critical', WATCHDOG_ERROR => 'error', WATCHDOG_WARNING => 'warning', WATCHDOG_NOTICE => 'notice', WATCHDOG_INFO => 'info', WATCHDOG_DEBUG => 'debug', ); } /** * Helper function to obtain the message types based on drupal version. * * @return * Array of watchdog message types. */ function drush_watchdog_message_types() { return drupal_map_assoc(_dblog_get_message_types()); } function _drush_theme_default() { return variable_get('theme_default', 'garland'); } function _drush_theme_admin() { return variable_get('admin_theme', drush_theme_get_default()); } function _drush_file_public_path() { if (function_exists('file_directory_path')) { return file_directory_path(); } } function _drush_file_private_path() { // @todo } /** * Gets the extension name. * * @param $info * The extension info. * @return string * The extension name. */ function _drush_extension_get_name($info) { return $info->name; } /** * Gets the extension type. * * @param $info * The extension info. * @return string * The extension type. */ function _drush_extension_get_type($info) { return $info->type; } /** * Gets the extension path. * * @param $info * The extension info. * @return string * The extension path. */ function _drush_extension_get_path($info) { return dirname($info->filename); } /* * Wrapper for CSRF token generation. */ function drush_get_token($value = NULL) { return drupal_get_token($value); } /* * Wrapper for _url(). */ function drush_url($path = NULL, $options = array()) { return url($path, $options); } /** * Output a Drupal render array, object or plain string as plain text. * * @param string $data * Data to render. * * @return string * The plain-text representation of the input. */ function drush_render($data) { if (is_array($data)) { $data = drupal_render($data); } $data = drupal_html_to_text($data); return $data; } $module) { if (isset($module->info['hidden'])) { unset($modules[$key]); } } } return $modules; } /** * Returns drupal required modules, including modules declared as required dynamically. */ function _drush_drupal_required_modules($module_info) { $required = drupal_required_modules(); foreach ($module_info as $name => $module) { if (isset($module->info['required']) && $module->info['required']) { $required[] = $name; } } return array_unique($required); } /** * Return dependencies and its status for modules. * * @param $modules * Array of module names * @param $module_info * Drupal 'files' array for modules as returned by drush_get_modules(). * @return * Array with dependencies and status for $modules */ function drush_check_module_dependencies($modules, $module_info) { $status = array(); foreach ($modules as $key => $module) { $dependencies = array_reverse($module_info[$module]->requires); $unmet_dependencies = array_diff(array_keys($dependencies), array_keys($module_info)); if (!empty($unmet_dependencies)) { $status[$key]['error'] = array( 'code' => 'DRUSH_PM_ENABLE_DEPENDENCY_NOT_FOUND', 'message' => dt('Module !module cannot be enabled because it depends on the following modules which could not be found: !unmet_dependencies', array('!module' => $module, '!unmet_dependencies' => implode(',', $unmet_dependencies))) ); } else { // check for version incompatibility foreach ($dependencies as $dependency_name => $v) { $current_version = $module_info[$dependency_name]->info['version']; $current_version = str_replace(drush_get_drupal_core_compatibility() . '-', '', $current_version); $incompatibility = drupal_check_incompatibility($v, $current_version); if (isset($incompatibility)) { $status[$key]['error'] = array( 'code' => 'DRUSH_PM_ENABLE_DEPENDENCY_VERSION_MISMATCH', 'message' => dt('Module !module cannot be enabled because it depends on !dependency !required_version but !current_version is available', array('!module' => $module, '!dependency' => $dependency_name, '!required_version' => $incompatibility, '!current_version' => $current_version)) ); } } } $status[$key]['unmet-dependencies'] = $unmet_dependencies; $status[$key]['dependencies'] = $dependencies; } return $status; } /** * Return dependents of modules. * * @param $modules * Array of module names * @param $module_info * Drupal 'files' array for modules as returned by drush_get_modules(). * @return * Array with dependents for each one of $modules */ function drush_module_dependents($modules, $module_info) { $dependents = array(); foreach ($modules as $module) { $dependents = array_merge($dependents, drupal_map_assoc(array_keys($module_info[$module]->required_by))); } return array_unique($dependents); } /** * Returns a list of enabled modules. * * This is a simplified version of module_list(). */ function drush_module_list() { $enabled = array(); $rsc = drush_db_select('system', 'name', 'type=:type AND status=:status', array(':type' => 'module', ':status' => 1)); while ($row = drush_db_result($rsc)) { $enabled[$row] = $row; } return $enabled; } /** * Return a list of extensions from a list of named extensions. * Both enabled and disabled/uninstalled extensions are returned. */ function drush_get_named_extensions_list($extensions) { $result = array(); $rsc = drush_db_select('system', array('name', 'status'), 'name IN (:extensions)', array(':extensions' => $extensions)); while ($row = drush_db_fetch_object($rsc)) { $result[$row->name] = $row; } return $result; } /** * Enable a list of modules. It is assumed the list contains all the dependencies not already enabled. * * @param $modules * Array of module names */ function drush_module_enable($modules) { // The list of modules already have all the dependencies, but they might not // be in the correct order. Still pass $enable_dependencies = TRUE so that // Drupal will enable the modules in the correct order. module_enable($modules); // Flush all caches. drupal_flush_all_caches(); } /** * Disable a list of modules. It is assumed the list contains all dependents not already disabled. * * @param $modules * Array of module names */ function drush_module_disable($modules) { // The list of modules already have all the dependencies, but they might not // be in the correct order. Still pass $enable_dependencies = TRUE so that // Drupal will enable the modules in the correct order. module_disable($modules); // Flush all caches. drupal_flush_all_caches(); } /** * Uninstall a list of modules. * * @param $modules * Array of module names */ function drush_module_uninstall($modules) { require_once DRUSH_DRUPAL_CORE . '/includes/install.inc'; // Break off 8.x functionality when we get another change. if (drush_drupal_major_version() >= 8) { module_uninstall($modules); } else { drupal_uninstall_modules($modules); } } /** * Checks that a given module exists and is enabled. * * @see module_exists(). * */ function drush_module_exists($module) { return module_exists($module); } /** * Determines which modules are implementing a hook. * */ function drush_module_implements($hook, $sort = FALSE, $reset = FALSE) { return module_implements($hook, $sort, $reset); } /** * Invokes a hook in a particular module. * */ function drush_module_invoke($module, $hook) { $args = func_get_args(); return call_user_func_array('module_invoke', $args); } /** * Invokes a hook in all enabled modules that implement it. * */ function drush_module_invoke_all($hook) { $args = func_get_args(); return call_user_func_array('module_invoke_all', $args); } /** * Returns a list of enabled themes. Use drush_get_themes() if you need to rebuild * and include hidden/disabled as well. * * @return array * A list of themes keyed by name. */ function drush_theme_list() { $enabled = array(); foreach (list_themes() as $key => $info) { if ($info->status) { $enabled[$key] = $info; } } return $enabled; } /** * Get complete information for all available themes. * * @param $include_hidden * Boolean to indicate whether hidden themes should be excluded or not. * @return * An array containing theme info for all available themes. */ function drush_get_themes($include_hidden = TRUE) { $themes = system_rebuild_theme_data(); if (!$include_hidden) { foreach ($themes as $key => $theme) { if (isset($theme->info['hidden'])) { unset($themes[$key]); } } } return $themes; } /** * Enable a list of themes. * * @param $themes * Array of theme names. */ function drush_theme_enable($themes) { theme_enable($themes); } /** * Disable a list of themes. * * @param $themes * Array of theme names. */ function drush_theme_disable($themes) { theme_disable($themes); } /** * Helper function to obtain the severity levels based on Drupal version. * * This is a copy of watchdog_severity_levels() without t(). * * Severity levels, as defined in RFC 3164: http://www.ietf.org/rfc/rfc3164.txt. * * @return * Array of watchdog severity levels. */ function drush_watchdog_severity_levels() { return array( WATCHDOG_EMERGENCY=> 'emergency', WATCHDOG_ALERT => 'alert', WATCHDOG_CRITICAL => 'critical', WATCHDOG_ERROR => 'error', WATCHDOG_WARNING => 'warning', WATCHDOG_NOTICE => 'notice', WATCHDOG_INFO => 'info', WATCHDOG_DEBUG => 'debug', ); } /** * Helper function to obtain the message types based on drupal version. * * @return * Array of watchdog message types. */ function drush_watchdog_message_types() { return drupal_map_assoc(_dblog_get_message_types()); } function _drush_theme_default() { return variable_get('theme_default', 'garland'); } function _drush_theme_admin() { return variable_get('admin_theme', drush_theme_get_default()); } function _drush_file_public_path() { return variable_get('file_public_path', conf_path() . '/files'); } function _drush_file_private_path() { return variable_get('file_private_path', FALSE); } /** * Gets the extension name. * * @param $info * The extension info. * @return string * The extension name. */ function _drush_extension_get_name($info) { return $info->name; } /** * Gets the extension type. * * @param $info * The extension info. * @return string * The extension type. */ function _drush_extension_get_type($info) { return $info->type; } /** * Gets the extension path. * * @param $info * The extension info. * @return string * The extension path. */ function _drush_extension_get_path($info) { return dirname($info->filename); } /* * Wrapper for CSRF token generation. */ function drush_get_token($value = NULL) { return drupal_get_token($value); } /* * Wrapper for _url(). */ function drush_url($path = NULL, array $options = array()) { return url($path, $options); } /** * Output a Drupal render array, object or plain string as plain text. * * @param string $data * Data to render. * * @return string * The plain-text representation of the input. */ function drush_render($data) { if (is_array($data)) { $data = drupal_render($data); } $data = drupal_html_to_text($data); return $data; } getStorage('image_style')->loadMultiple(); } function drush_image_style_load($style_name) { return \Drupal::entityManager()->getStorage('image_style')->load($style_name); } function drush_image_flush_single($style_name) { if ($style = drush_image_style_load($style_name)) { $style->flush(); drush_log(dt('Image style !style_name flushed', array('!style_name' => $style_name)), LogLevel::SUCCESS); } } /* * Command callback. Create an image derivative. * * @param string $style_name * The name of an image style. * * @param string $source * The path to a source image, relative to Drupal root. */ function _drush_image_derive($style_name, $source) { $image_style = drush_image_style_load($style_name); $derivative_uri = $image_style->buildUri($source); if ($image_style->createDerivative($source, $derivative_uri)) { return $derivative_uri; } } $style_name)), LogLevel::SUCCESS); } } /* * Command callback. Create an image derivative. * * @param string $style_name * The name of an image style. * * @param string $source * The path to a source image, relative to Drupal root. */ function _drush_image_derive($style_name, $source) { $image_style = image_style_load($style_name); $scheme = file_default_scheme(); $image_uri = $scheme . '://' . $source; $derivative_uri = image_style_path($image_style['name'], $image_uri); if (image_style_create_derivative($image_style, $source, $derivative_uri)) { return $derivative_uri; } } $name)), LogLevel::WARNING); } // Discard already disabled extensions. foreach ($extensions as $name) { if (!$extension_info[$name]->status) { if ($extension_info[$name]->type == 'module') { unset($modules[$name]); } else { unset($themes[$name]); } drush_log(dt('!extension is already disabled.', array('!extension' => $name)), LogLevel::OK); } } // Discard default theme. if (!empty($themes)) { $default_theme = drush_theme_get_default(); if (in_array($default_theme, $themes)) { unset($themes[$default_theme]); drush_log(dt('!theme is the default theme and can\'t be disabled.', array('!theme' => $default_theme)), LogLevel::OK); } } if (!empty($modules)) { // Add enabled dependents to the list of modules to disable. $dependents = drush_module_dependents($modules, $extension_info); $dependents = array_intersect($dependents, drush_module_list()); $modules = array_merge($modules, $dependents); // Discard required modules. $required = drush_drupal_required_modules($extension_info); foreach ($required as $module) { if (isset($modules[$module])) { unset($modules[$module]); $info = $extension_info[$module]->info; // No message for hidden modules. if (!isset($info['hidden'])) { $explanation = !empty($info['explanation']) ? ' ' . dt('Reason: !explanation.', array('!explanation' => strip_tags($info['explanation']))) : ''; drush_log(dt('!module is a required module and can\'t be disabled.', array('!module' => $module)) . $explanation, LogLevel::OK); } } } } // Inform the user which extensions will finally be disabled. $extensions = array_merge($modules, $themes); if (empty($extensions)) { return drush_log(dt('There were no extensions that could be disabled.'), LogLevel::OK); } else { drush_print(dt('The following extensions will be disabled: !extensions', array('!extensions' => implode(', ', $extensions)))); if(!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } } // Disable themes. if (!empty($themes)) { drush_theme_disable($themes); } // Disable modules and pass dependency validation in form submit. if (!empty($modules)) { drush_module_disable($modules); } // Inform the user of final status. $result_extensions = drush_get_named_extensions_list($extensions); $problem_extensions = array(); foreach ($result_extensions as $extension) { if (!$extension->status) { drush_log(dt('!extension was disabled successfully.', array('!extension' => $extension->name)), LogLevel::OK); } else { $problem_extensions[] = $extension->name; } } if (!empty($problem_extensions)) { return drush_set_error('DRUSH_PM_DISABLE_EXTENSION_ISSUE', dt('There was a problem disabling !extension.', array('!extension' => implode(',', $problem_extensions)))); } } /** * Command callback. Uninstall one or more modules. */ function _drush_pm_uninstall($modules) { drush_include_engine('drupal', 'environment'); $module_info = drush_get_modules(); $required = drush_drupal_required_modules($module_info); // Discards modules which are enabled, not found or already uninstalled. foreach ($modules as $key => $module) { if (!isset($module_info[$module])) { // The module does not exist in the system. unset($modules[$key]); drush_log(dt('Module !module was not found and will not be uninstalled.', array('!module' => $module)), LogLevel::WARNING); } else if ($module_info[$module]->status) { // The module is enabled. unset($modules[$key]); drush_log(dt('!module is not disabled. Use `pm-disable` command before `pm-uninstall`.', array('!module' => $module)), LogLevel::WARNING); } else if ($module_info[$module]->schema_version == -1) { // SCHEMA_UNINSTALLED // The module is uninstalled. unset($modules[$key]); drush_log(dt('!module is already uninstalled.', array('!module' => $module)), LogLevel::OK); } else { $dependents = array(); foreach (drush_module_dependents(array($module), $module_info) as $dependent) { if (!in_array($dependent, $required) && ($module_info[$dependent]->schema_version != -1)) { $dependents[] = $dependent; } } if (count($dependents)) { drush_log(dt('To uninstall !module, the following modules must be uninstalled first: !required', array('!module' => $module, '!required' => implode(', ', $dependents))), LogLevel::ERROR); unset($modules[$key]); } } } // Inform the user which modules will finally be uninstalled. if (empty($modules)) { return drush_log(dt('There were no modules that could be uninstalled.'), LogLevel::OK); } else { drush_print(dt('The following modules will be uninstalled: !modules', array('!modules' => implode(', ', $modules)))); if(!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } } // Uninstall the modules. drush_module_uninstall($modules); // Inform the user of final status. foreach ($modules as $module) { drush_log(dt('!module was successfully uninstalled.', array('!module' => $module)), LogLevel::OK); } } $extension)), LogLevel::WARNING); } elseif (in_array($extension, $required)) { unset($extensions[$extension]); $info = $extension_info[$extension]->info; $explanation = !empty($info['explanation']) ? ' ' . dt('Reason: !explanation.', array('!explanation' => strip_tags($info['explanation']))) : ''; drush_log(dt('!extension is a required extension and can\'t be uninstalled.', array('!extension' => $extension)) . $explanation, LogLevel::OK); } elseif (!$extension_info[$extension]->status) { unset($extensions[$extension]); drush_log(dt('!extension is already uninstalled.', array('!extension' => $extension)), LogLevel::OK); } elseif (drush_extension_get_type($extension_info[$extension]) == 'module') { // Add installed dependencies to the list of modules to uninstall. foreach (drush_module_dependents(array($extension), $extension_info) as $dependent) { // Check if this dependency is not required, already enabled, and not already already in the list of modules to uninstall. if (!in_array($dependent, $required) && ($extension_info[$dependent]->status) && !in_array($dependent, $extensions)) { $extensions[] = $dependent; } } } } // Discard default theme. $default_theme = drush_theme_get_default(); if (in_array($default_theme, $extensions)) { unset($extensions[$default_theme]); drush_log(dt('!theme is the default theme and can\'t be uninstalled.', array('!theme' => $default_theme)), LogLevel::OK); } // Inform the user which extensions will finally be disabled. if (empty($extensions)) { return drush_log(dt('There were no extensions that could be uninstalled.'), LogLevel::OK); } else { drush_print(dt('The following extensions will be uninstalled: !extensions', array('!extensions' => implode(', ', $extensions)))); if(!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } } // Classify extensions in themes and modules. $modules = array(); $themes = array(); drush_pm_classify_extensions($extensions, $modules, $themes, $extension_info); drush_module_uninstall($modules); drush_theme_uninstall($themes); // Inform the user of final status. foreach ($extensions as $extension) { drush_log(dt('!extension was successfully uninstalled.', array('!extension' => $extension)), LogLevel::OK); } } FALSE) + install_state_defaults(); try { install_begin_request($class_loader, $install_state); $profile = _install_select_profile($install_state); } catch (\Exception $e) { // This is only a best effort to provide a better default, no harm done // if it fails. } if (empty($profile)) { $profile = 'standard'; } } $sql = drush_sql_get_class(); $db_spec = $sql->db_spec(); $account_name = drush_get_option('account-name', 'admin'); $account_pass = drush_get_option('account-pass', FALSE); $show_password = drush_get_option('show-passwords', !$account_pass); if (!$account_pass) { $account_pass = drush_generate_password(); } $settings = array( 'parameters' => array( 'profile' => $profile, 'langcode' => drush_get_option('locale', 'en'), ), 'forms' => array( 'install_settings_form' => array( 'driver' => $db_spec['driver'], $db_spec['driver'] => $db_spec, 'op' => dt('Save and continue'), ), 'install_configure_form' => array( 'site_name' => drush_get_option('site-name', 'Site-Install'), 'site_mail' => drush_get_option('site-mail', 'admin@example.com'), 'account' => array( 'name' => $account_name, 'mail' => drush_get_option('account-mail', 'admin@example.com'), 'pass' => array( 'pass1' => $account_pass, 'pass2' => $account_pass, ), ), 'enable_update_status_module' => TRUE, 'enable_update_status_emails' => TRUE, 'clean_url' => drush_get_option('clean-url', TRUE), 'op' => dt('Save and continue'), ), ), ); // Merge in the additional options. foreach ($additional_form_options as $key => $value) { $current = &$settings['forms']; foreach (explode('.', $key) as $param) { $current = &$current[$param]; } $current = $value; } $msg = 'Starting Drupal installation. This takes a while.'; if (is_null(drush_get_option('notify'))) { $msg .= ' Consider using the --notify global option.'; } drush_log(dt($msg), LogLevel::OK); drush_op('install_drupal', $class_loader, $settings); if ($show_password) { drush_log(dt('Installation complete. User name: @name User password: @pass', array('@name' => $account_name, '@pass' => $account_pass)), LogLevel::OK); } else { drush_log(dt('Installation complete.'), LogLevel::OK); } } &1 // redirect added to the command in drush_shell_exec(). We will actually take out // all but fatal errors. See http://drupal.org/node/985716 for more information. $phpcode = 'error_reporting(E_ERROR);' . _drush_site_install6_cookies($profile). ' include("'. $drupal_root .'/install.php");'; drush_shell_exec('php -r %s', $phpcode); $cli_output = drush_shell_exec_output(); $cli_cookie = end($cli_output); // We need to bootstrap the database to be able to check the progress of the // install batch process since we're not duplicating the install process using // drush_batch functions, but calling the process directly. drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_DATABASE); $status = _drush_site_install6_stage($profile, $cli_cookie, "start"); if ($status === FALSE) { return FALSE; } $status = _drush_site_install6_stage($profile, $cli_cookie, "do_nojs"); if ($status === FALSE) { return FALSE; } $status = _drush_site_install6_stage($profile, $cli_cookie, "finished"); if ($status === FALSE) { return FALSE; } $account_name = drush_get_option('account-name', 'admin'); $account_pass = drush_get_option('account-pass', FALSE); $show_password = drush_get_option('show-passwords', !$account_pass); if (!$account_pass) { $account_pass = drush_generate_password(); } $phpcode = _drush_site_install6_cookies($profile, $cli_cookie); $post = array ( "site_name" => drush_get_option('site-name', 'Site-Install'), "site_mail" => drush_get_option('site-mail', 'admin@example.com'), "account" => array ( "name" => $account_name, "mail" => drush_get_option('account-mail', 'admin@example.com'), "pass" => array ( "pass1" => $account_pass, "pass2" => $account_pass, ) ), "date_default_timezone" => "0", "clean_url" => drush_get_option('clean-url', TRUE), "form_id" => "install_configure_form", "update_status_module" => array("1" => "1"), ); // Merge in the additional options. foreach ($additional_form_options as $key => $value) { $current = &$post; foreach (explode('.', $key) as $param) { $current = &$current[$param]; } $current = $value; } $phpcode .= ' $_POST = ' . var_export($post, true) . '; include("'. $drupal_root .'/install.php");'; drush_shell_exec('php -r %s', $phpcode); if ($show_password) { drush_log(dt('Installation complete. User name: @name User password: @pass', array('@name' => $account_name, '@pass' => $account_pass)), LogLevel::OK); } else { drush_log(dt('Installation complete.'), LogLevel::OK); } } /** * Submit a given op to install.php; if a meta "Refresh" tag * is returned in the result, then submit that op as well. */ function _drush_site_install6_stage($profile, $cli_cookie, $initial_op) { $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT'); // Remember the install task at the start of the stage $install_task = _drush_site_install6_install_task(); $op = $initial_op; while (!empty($op)) { $phpcode = _drush_site_install6_cookies($profile, $cli_cookie). ' $_GET["op"]="' . $op . '"; include("'. $drupal_root .'/install.php");'; drush_shell_exec('php -r %s', $phpcode); $output = implode("\n", drush_shell_exec_output()); // Check for a "Refresh" back to the do_nojs op; e.g.: // // If this pattern is NOT found, then go on to the "finished" step. $matches = array(); $match_result = preg_match('/http-equiv="Refresh".*op=([a-zA-Z0-9_]*)/', $output, $matches); if ($match_result) { $op = $matches[1]; } else { $op = ''; } } if (($install_task == _drush_site_install6_install_task()) && ($initial_op != "finished")) { return drush_set_error('DRUSH_SITE_INSTALL_FAILED', dt("The site install task '!task' failed.", array('!task' => $install_task))); } return TRUE; } /** * Utility function to grab/set current "cli cookie". */ function _drush_site_install6_cookies($profile, $cookie = NULL) { $drupal_base_url = parse_url(drush_get_context('DRUSH_SELECTED_URI')); $output = '$_GET=array("profile"=>"' . $profile . '", "locale"=>"' . drush_get_option('locale', 'en') . '", "id"=>"1"); $_REQUEST=&$_GET;'; $output .= 'define("DRUSH_SITE_INSTALL6", TRUE);$_SERVER["SERVER_SOFTWARE"] = NULL;'; $output .= '$_SERVER["SCRIPT_NAME"] = "/install.php";'; $output .= '$_SERVER["HTTP_HOST"] = "'.$drupal_base_url['host'].'";'; $output .= '$_SERVER["REMOTE_ADDR"] = "127.0.0.1";'; if ($cookie) { $output .= sprintf('$_COOKIE=unserialize("%s");', str_replace('"', '\"', $cookie)); } else { $output .= 'function _cli_cookie_print(){print(serialize(array(session_name()=>session_id())));} register_shutdown_function("_cli_cookie_print");'; } return $output; } /** * Utility function to check the install_task. We are * not bootstrapped to a high enough level to use variable_get. */ function _drush_site_install6_install_task() { if ($data = db_result(db_query("SELECT value FROM {variable} WHERE name = 'install_task'",1))) { $result = unserialize($data); } return $result; } db_spec(); $account_name = drush_get_option('account-name', 'admin'); $account_pass = drush_get_option('account-pass', FALSE); $show_password = drush_get_option('show-passwords', !$account_pass); if (!$account_pass) { $account_pass = drush_generate_password(); } $settings = array( 'parameters' => array( 'profile' => $profile, 'locale' => drush_get_option('locale', 'en'), ), 'forms' => array( 'install_settings_form' => array( 'driver' => $db_spec['driver'], $db_spec['driver'] => $db_spec, 'op' => dt('Save and continue'), ), 'install_configure_form' => array( 'site_name' => drush_get_option('site-name', 'Site-Install'), 'site_mail' => drush_get_option('site-mail', 'admin@example.com'), 'account' => array( 'name' => $account_name, 'mail' => drush_get_option('account-mail', 'admin@example.com'), 'pass' => array( 'pass1' => $account_pass, 'pass2' => $account_pass, ), ), 'update_status_module' => array( 1 => TRUE, 2 => TRUE, ), 'clean_url' => drush_get_option('clean-url', TRUE), 'op' => dt('Save and continue'), ), ), ); // Merge in the additional options. foreach ($additional_form_options as $key => $value) { $current = &$settings['forms']; foreach (explode('.', $key) as $param) { $current = &$current[$param]; } $current = $value; } $msg = 'Starting Drupal installation. This takes a while.'; if (is_null(drush_get_option('notify'))) { $msg .= ' Consider using the --notify global option.'; } drush_log(dt($msg), LogLevel::OK); drush_op('install_drupal', $settings); if ($show_password) { drush_log(dt('Installation complete. User name: @name User password: @pass', array('@name' => $account_name, '@pass' => $account_pass)), LogLevel::OK); } else { drush_log(dt('Installation complete.'), LogLevel::OK); } } FALSE, 'query' => 'What went wrong'); * The schema version will not be updated in this case, and all the * aborted updates will continue to appear on update.php as updates that * have not yet been run. * * @param $module * The module whose update will be run. * @param $number * The update number to run. * @param $context * The batch context array */ function drush_update_do_one($module, $number, $dependency_map, &$context) { $function = $module . '_update_' . $number; // If this update was aborted in a previous step, or has a dependency that // was aborted in a previous step, go no further. if (!empty($context['results']['#abort']) && array_intersect($context['results']['#abort'], array_merge($dependency_map, array($function)))) { return; } $context['log'] = FALSE; \Drupal::moduleHandler()->loadInclude($module, 'install'); $ret = array(); if (function_exists($function)) { try { if ($context['log']) { Database::startLog($function); } drush_log("Executing " . $function); $ret['results']['query'] = $function($context['sandbox']); $ret['results']['success'] = TRUE; } // @TODO We may want to do different error handling for different exception // types, but for now we'll just print the message. catch (Exception $e) { $ret['#abort'] = array('success' => FALSE, 'query' => $e->getMessage()); drush_set_error('DRUPAL_EXCEPTION', $e->getMessage()); } if ($context['log']) { $ret['queries'] = Database::getLog($function); } } else { $ret['#abort'] = array('success' => FALSE); drush_set_error('DRUSH_UPDATE_FUNCTION_NOT_FOUND', dt('Update function @function not found', array('@function' => $function))); } if (isset($context['sandbox']['#finished'])) { $context['finished'] = $context['sandbox']['#finished']; unset($context['sandbox']['#finished']); } if (!isset($context['results'][$module])) { $context['results'][$module] = array(); } if (!isset($context['results'][$module][$number])) { $context['results'][$module][$number] = array(); } $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret); if (!empty($ret['#abort'])) { // Record this function in the list of updates that were aborted. $context['results']['#abort'][] = $function; } // Record the schema update if it was completed successfully. if ($context['finished'] == 1 && empty($ret['#abort'])) { drupal_set_installed_schema_version($module, $number); } $context['message'] = 'Performing ' . $function; } /** * Clears caches and rebuilds the container. * * This is called in between regular updates and post updates. Do not use * drush_drupal_cache_clear_all() as the cache clearing and container rebuild * must happen in the same process that the updates are run in. * * Drupal core's update.php uses drupal_flush_all_caches() directly without * explicitly rebuilding the container as the container is rebuilt on the next * HTTP request of the batch. * * @see drush_drupal_cache_clear_all() * @see \Drupal\system\Controller\DbUpdateController::triggerBatch() */ function drush_update_cache_rebuild() { drupal_flush_all_caches(); \Drupal::service('kernel')->rebuildContainer(); } function update_main() { // In D8, we expect to be in full bootstrap. drush_bootstrap_to_phase(DRUSH_BOOTSTRAP_DRUPAL_FULL); require_once DRUPAL_ROOT . '/core/includes/install.inc'; require_once DRUPAL_ROOT . '/core/includes/update.inc'; drupal_load_updates(); update_fix_compatibility(); // Pending hook_update_N() implementations. $pending = update_get_update_list(); // Pending hook_post_update_X() implementations. $post_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateInformation(); $start = array(); $change_summary = []; if (drush_get_option('entity-updates', FALSE)) { $change_summary = \Drupal::entityDefinitionUpdateManager()->getChangeSummary(); } // Print a list of pending updates for this module and get confirmation. if (count($pending) || count($change_summary) || count($post_updates)) { drush_print(dt('The following updates are pending:')); drush_print(); foreach ($change_summary as $entity_type_id => $changes) { drush_print($entity_type_id . ' entity type : '); foreach ($changes as $change) { drush_print(strip_tags($change), 2); } } foreach (array('update', 'post_update') as $update_type) { $updates = $update_type == 'update' ? $pending : $post_updates; foreach ($updates as $module => $updates) { if (isset($updates['start'])) { drush_print($module . ' module : '); if (!empty($updates['pending'])) { $start += [$module => array()]; $start[$module] = array_merge($start[$module], $updates['pending']); foreach ($updates['pending'] as $update) { drush_print(strip_tags($update), 2); } } drush_print(); } } } if (!drush_confirm(dt('Do you wish to run all pending updates?'))) { return drush_user_abort(); } drush_update_batch($start); } else { drush_log(dt("No database updates required"), LogLevel::SUCCESS); } return count($pending) + count($change_summary) + count($post_updates); } function _update_batch_command($id) { // In D8, we expect to be in full bootstrap. drush_bootstrap_to_phase(DRUSH_BOOTSTRAP_DRUPAL_FULL); drush_batch_command($id); } /** * Start the database update batch process. */ function drush_update_batch() { $start = drush_get_update_list(); // Resolve any update dependencies to determine the actual updates that will // be run and the order they will be run in. $updates = update_resolve_dependencies($start); // Store the dependencies for each update function in an array which the // batch API can pass in to the batch operation each time it is called. (We // do not store the entire update dependency array here because it is // potentially very large.) $dependency_map = array(); foreach ($updates as $function => $update) { $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array(); } $operations = array(); foreach ($updates as $update) { if ($update['allowed']) { // Set the installed version of each module so updates will start at the // correct place. (The updates are already sorted, so we can simply base // this on the first one we come across in the above foreach loop.) if (isset($start[$update['module']])) { drupal_set_installed_schema_version($update['module'], $update['number'] - 1); unset($start[$update['module']]); } // Add this update function to the batch. $function = $update['module'] . '_update_' . $update['number']; $operations[] = array('drush_update_do_one', array($update['module'], $update['number'], $dependency_map[$function])); } } // Apply post update hooks. $post_updates = \Drupal::service('update.post_update_registry')->getPendingUpdateFunctions(); if ($post_updates) { $operations[] = ['drush_update_cache_rebuild', []]; foreach ($post_updates as $function) { $operations[] = ['update_invoke_post_update', [$function]]; } } // Lastly, perform entity definition updates, which will update storage // schema if needed. If module update functions need to work with specific // entity schema they should call the entity update service for the specific // update themselves. // @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyEntityUpdate() // @see \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface::applyFieldUpdate() if (drush_get_option('entity-updates', FALSE) && \Drupal::entityDefinitionUpdateManager()->needsUpdates()) { $operations[] = array('drush_update_entity_definitions', array()); } $batch['operations'] = $operations; $batch += array( 'title' => 'Updating', 'init_message' => 'Starting updates', 'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.', 'finished' => 'drush_update_finished', 'file' => 'includes/update.inc', ); batch_set($batch); \Drupal::service('state')->set('system.maintenance_mode', TRUE); drush_backend_batch_process('updatedb-batch-process'); \Drupal::service('state')->set('system.maintenance_mode', FALSE); } /** * Apply entity schema updates. */ function drush_update_entity_definitions(&$context) { try { \Drupal::entityDefinitionUpdateManager()->applyUpdates(); } catch (EntityStorageException $e) { watchdog_exception('update', $e); $variables = Error::decodeException($e); unset($variables['backtrace']); // The exception message is run through // \Drupal\Component\Utility\SafeMarkup::checkPlain() by // \Drupal\Core\Utility\Error::decodeException(). $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables)); $context['results']['core']['update_entity_definitions'] = $ret; $context['results']['#abort'][] = 'update_entity_definitions'; } } // Copy of protected \Drupal\system\Controller\DbUpdateController::getModuleUpdates. function drush_get_update_list() { $return = array(); $updates = update_get_update_list(); foreach ($updates as $module => $update) { $return[$module] = $update['start']; } return $return; } /** * Process and display any returned update output. * * @see \Drupal\system\Controller\DbUpdateController::batchFinished() * @see \Drupal\system\Controller\DbUpdateController::results() */ function drush_update_finished($success, $results, $operations) { if (!drush_get_option('cache-clear', TRUE)) { drush_log(dt("Skipping cache-clear operation due to --cache-clear=0 option."), LogLevel::WARNING); } else { drupal_flush_all_caches(); } foreach ($results as $module => $updates) { if ($module != '#abort') { foreach ($updates as $number => $queries) { foreach ($queries as $query) { // If there is no message for this update, don't show anything. if (empty($query['query'])) { continue; } if ($query['success']) { drush_log(strip_tags($query['query'])); } else { drush_set_error(dt('Failed: ') . strip_tags($query['query'])); } } } } } } /** * Return a 2 item array with * - an array where each item is a 3 item associative array describing a pending update. * - an array listing the first update to run, keyed by module. */ function updatedb_status() { $pending = update_get_update_list(); $return = array(); // Ensure system module's updates run first. $start['system'] = array(); foreach (\Drupal::entityDefinitionUpdateManager()->getChangeSummary() as $entity_type_id => $changes) { foreach ($changes as $change) { $return[] = array( 'module' => dt('@type entity type', array('@type' => $entity_type_id)), 'update_id' => '', 'description' => strip_tags($change)); } } // Print a list of pending updates for this module and get confirmation. foreach ($pending as $module => $updates) { if (isset($updates['start'])) { foreach ($updates['pending'] as $update_id => $description) { // Strip cruft from front. $description = str_replace($update_id . ' - ', '', $description); $return[] = array('module' => ucfirst($module), 'update_id' => $update_id, 'description' => $description); } if (isset($updates['start'])) { $start[$module] = $updates['start']; } } } return array($return, $start); } /** * Apply pending entity schema updates. */ function entity_updates_main() { $change_summary = \Drupal::entityDefinitionUpdateManager()->getChangeSummary(); if (!empty($change_summary)) { drush_print(dt('The following updates are pending:')); drush_print(); foreach ($change_summary as $entity_type_id => $changes) { drush_print($entity_type_id . ' entity type : '); foreach ($changes as $change) { drush_print(strip_tags($change), 2); } } if (!drush_confirm(dt('Do you wish to run all pending updates?'))) { return drush_user_abort(); } $operations[] = array('drush_update_entity_definitions', array()); $batch['operations'] = $operations; $batch += array( 'title' => 'Updating', 'init_message' => 'Starting updates', 'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.', 'finished' => 'drush_update_finished', 'file' => 'includes/update.inc', ); batch_set($batch); \Drupal::service('state')->set('system.maintenance_mode', TRUE); drush_backend_batch_process('updatedb-batch-process'); \Drupal::service('state')->set('system.maintenance_mode', FALSE); } else { drush_log(dt("No entity schema updates required"), LogLevel::SUCCESS); } } "''" * in the $attributes array. If NOT NULL and DEFAULT are set the PostgreSQL * version will set values of the added column in old rows to the * DEFAULT value. * * @param $ret * Array to which results will be added. * @param $table * Name of the table, without {} * @param $column * Name of the column * @param $type * Type of column * @param $attributes * Additional optional attributes. Recognized attributes: * not null => TRUE|FALSE * default => NULL|FALSE|value (the value must be enclosed in '' marks) * @return * nothing, but modifies $ret parameter. */ function db_add_column(&$ret, $table, $column, $type, $attributes = array()) { if (array_key_exists('not null', $attributes) and $attributes['not null']) { $not_null = 'NOT NULL'; } if (array_key_exists('default', $attributes)) { if (!isset($attributes['default'])) { $default_val = 'NULL'; $default = 'default NULL'; } elseif ($attributes['default'] === FALSE) { $default = ''; } else { $default_val = "$attributes[default]"; $default = "default $attributes[default]"; } } $ret[] = update_sql("ALTER TABLE {". $table ."} ADD $column $type"); if (!empty($default)) { $ret[] = update_sql("ALTER TABLE {". $table ."} ALTER $column SET $default"); } if (!empty($not_null)) { if (!empty($default)) { $ret[] = update_sql("UPDATE {". $table ."} SET $column = $default_val"); } $ret[] = update_sql("ALTER TABLE {". $table ."} ALTER $column SET NOT NULL"); } } /** * Change a column definition using syntax appropriate for PostgreSQL. * Save result of SQL commands in $ret array. * * Remember that changing a column definition involves adding a new column * and dropping an old one. This means that any indices, primary keys and * sequences from serial-type columns are dropped and might need to be * recreated. * * @param $ret * Array to which results will be added. * @param $table * Name of the table, without {} * @param $column * Name of the column to change * @param $column_new * New name for the column (set to the same as $column if you don't want to change the name) * @param $type * Type of column * @param $attributes * Additional optional attributes. Recognized attributes: * not null => TRUE|FALSE * default => NULL|FALSE|value (with or without '', it won't be added) * @return * nothing, but modifies $ret parameter. */ function db_change_column(&$ret, $table, $column, $column_new, $type, $attributes = array()) { if (array_key_exists('not null', $attributes) and $attributes['not null']) { $not_null = 'NOT NULL'; } if (array_key_exists('default', $attributes)) { if (!isset($attributes['default'])) { $default_val = 'NULL'; $default = 'default NULL'; } elseif ($attributes['default'] === FALSE) { $default = ''; } else { $default_val = "$attributes[default]"; $default = "default $attributes[default]"; } } $ret[] = update_sql("ALTER TABLE {". $table ."} RENAME $column TO ". $column ."_old"); $ret[] = update_sql("ALTER TABLE {". $table ."} ADD $column_new $type"); $ret[] = update_sql("UPDATE {". $table ."} SET $column_new = ". $column ."_old"); if ($default) { $ret[] = update_sql("ALTER TABLE {". $table ."} ALTER $column_new SET $default"); } if ($not_null) { $ret[] = update_sql("ALTER TABLE {". $table ."} ALTER $column_new SET NOT NULL"); } $ret[] = update_sql("ALTER TABLE {". $table ."} DROP ". $column ."_old"); } /** * Disable anything in the {system} table that is not compatible with the * current version of Drupal core. */ function update_fix_compatibility() { $ret = array(); $incompatible = array(); $query = db_query("SELECT name, type, status FROM {system} WHERE status = 1 AND type IN ('module','theme')"); while ($result = db_fetch_object($query)) { if (update_check_incompatibility($result->name, $result->type)) { $incompatible[] = $result->name; drush_log(dt("!type !name is incompatible with this release of Drupal, and will be disabled.", array("!type" => $result->type, '!name' => $result->name)), LogLevel::WARNING); } } if (!empty($incompatible)) { $ret[] = update_sql("UPDATE {system} SET status = 0 WHERE name IN ('". implode("','", $incompatible) ."')"); } return $ret; } /** * Helper function to test compatibility of a module or theme. */ function update_check_incompatibility($name, $type = 'module') { static $themes, $modules; // Store values of expensive functions for future use. if (empty($themes) || empty($modules)) { drush_include_engine('drupal', 'environment'); $themes = _system_theme_data(); $modules = module_rebuild_cache(); } if ($type == 'module' && isset($modules[$name])) { $file = $modules[$name]; } else if ($type == 'theme' && isset($themes[$name])) { $file = $themes[$name]; } if (!isset($file) || !isset($file->info['core']) || $file->info['core'] != drush_get_drupal_core_compatibility() || version_compare(phpversion(), $file->info['php']) < 0) { return TRUE; } return FALSE; } /** * Perform Drupal 5.x to 6.x updates that are required for update.php * to function properly. * * This function runs when update.php is run the first time for 6.x, * even before updates are selected or performed. It is important * that if updates are not ultimately performed that no changes are * made which make it impossible to continue using the prior version. * Just adding columns is safe. However, renaming the * system.description column to owner is not. Therefore, we add the * system.owner column and leave it to system_update_6008() to copy * the data from description and remove description. The same for * renaming locales_target.locale to locales_target.language, which * will be finished by locale_update_6002(). */ function update_fix_d6_requirements() { $ret = array(); if (drupal_get_installed_schema_version('system') < 6000 && !variable_get('update_d6_requirements', FALSE)) { $spec = array('type' => 'int', 'size' => 'small', 'default' => 0, 'not null' => TRUE); db_add_field($ret, 'cache', 'serialized', $spec); db_add_field($ret, 'cache_filter', 'serialized', $spec); db_add_field($ret, 'cache_page', 'serialized', $spec); db_add_field($ret, 'cache_menu', 'serialized', $spec); db_add_field($ret, 'system', 'info', array('type' => 'text')); db_add_field($ret, 'system', 'owner', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => '')); if (db_table_exists('locales_target')) { db_add_field($ret, 'locales_target', 'language', array('type' => 'varchar', 'length' => 12, 'not null' => TRUE, 'default' => '')); } if (db_table_exists('locales_source')) { db_add_field($ret, 'locales_source', 'textgroup', array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => 'default')); db_add_field($ret, 'locales_source', 'version', array('type' => 'varchar', 'length' => 20, 'not null' => TRUE, 'default' => 'none')); } variable_set('update_d6_requirements', TRUE); // Create the cache_block table. See system_update_6027() for more details. $schema['cache_block'] = array( 'fields' => array( 'cid' => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), 'data' => array('type' => 'blob', 'not null' => FALSE, 'size' => 'big'), 'expire' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), 'created' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), 'headers' => array('type' => 'text', 'not null' => FALSE), 'serialized' => array('type' => 'int', 'size' => 'small', 'not null' => TRUE, 'default' => 0) ), 'indexes' => array('expire' => array('expire')), 'primary key' => array('cid'), ); db_create_table($ret, 'cache_block', $schema['cache_block']); // Create the semaphore table now -- the menu system after 6.15 depends on // this table, and menu code runs in updates prior to the table being // created in its original update function, system_update_6054(). $schema['semaphore'] = array( 'fields' => array( 'name' => array( 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), 'value' => array( 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''), 'expire' => array( 'type' => 'float', 'size' => 'big', 'not null' => TRUE), ), 'indexes' => array('expire' => array('expire')), 'primary key' => array('name'), ); db_create_table($ret, 'semaphore', $schema['semaphore']); } return $ret; } /** * Check update requirements and report any errors. */ function update_check_requirements() { // Check the system module requirements only. $requirements = module_invoke('system', 'requirements', 'update'); $severity = drupal_requirements_severity($requirements); // If there are issues, report them. if ($severity != REQUIREMENT_OK) { foreach ($requirements as $requirement) { if (isset($requirement['severity']) && $requirement['severity'] != REQUIREMENT_OK) { $message = isset($requirement['description']) ? $requirement['description'] : ''; if (isset($requirement['value']) && $requirement['value']) { $message .= ' (Currently using '. $requirement['title'] .' '. $requirement['value'] .')'; } drush_log($message, LogLevel::WARNING); } } } } /** * Create the batch table. * * This is part of the Drupal 5.x to 6.x migration. */ function update_create_batch_table() { // If batch table exists, update is not necessary if (db_table_exists('batch')) { return; } $schema['batch'] = array( 'fields' => array( 'bid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE), 'token' => array('type' => 'varchar', 'length' => 64, 'not null' => TRUE), 'timestamp' => array('type' => 'int', 'not null' => TRUE), 'batch' => array('type' => 'text', 'not null' => FALSE, 'size' => 'big') ), 'primary key' => array('bid'), 'indexes' => array('token' => array('token')), ); $ret = array(); db_create_table($ret, 'batch', $schema['batch']); return $ret; } function update_main_prepare() { global $profile; // Some unavoidable errors happen because the database is not yet up-to-date. // Our custom error handler is not yet installed, so we just suppress them. drush_errors_off(); require_once './includes/bootstrap.inc'; // Minimum load of components. // This differs from the Drupal 6 update.php workflow for compatbility with // the Drupal 6 backport of module_implements() caching. // @see http://drupal.org/node/557542 drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_DATABASE); require_once './includes/install.inc'; require_once './includes/file.inc'; require_once './modules/system/system.install'; // Load module basics. include_once './includes/module.inc'; $module_list['system']['filename'] = 'modules/system/system.module'; $module_list['filter']['filename'] = 'modules/filter/filter.module'; module_list(TRUE, FALSE, FALSE, $module_list); module_implements('', FALSE, TRUE); drupal_load('module', 'system'); drupal_load('module', 'filter'); // Set up $language, since the installer components require it. drupal_init_language(); // Set up theme system for the maintenance page. drupal_maintenance_theme(); // Check the update requirements for Drupal. update_check_requirements(); drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_FULL); $profile = variable_get('install_profile', 'default'); // Updates only run reliably if user ID #1 is logged in. For example, node_delete() requires elevated perms in D5/6. if (!drush_get_context('DRUSH_USER')) { drush_set_option('user', 1); drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_LOGIN); } // This must happen *after* drupal_bootstrap(), since it calls // variable_(get|set), which only works after a full bootstrap. _drush_log_update_sql(update_create_batch_table()); // Turn error reporting back on. From now on, only fatal errors (which are // not passed through the error handler) will cause a message to be printed. drush_errors_on(); // Perform Drupal 5.x to 6.x updates that are required for update.php to function properly. _drush_log_update_sql(update_fix_d6_requirements()); // Must unset $theme->status in order to safely rescan and repopulate // the system table to ensure we have a full picture of the platform. // This is needed because $theme->status is set to 0 in a call to // list_themes() done by drupal_maintenance_theme(). // It is a issue with _system_theme_data() that returns its own cache // variable and can be modififed by others. When this is fixed in // drupal core we can remove this unset. // For reference see: http://drupal.org/node/762754 $themes = _system_theme_data(); foreach ($themes as $theme) { unset($theme->status); } drush_get_extensions(); include_once './includes/batch.inc'; drupal_load_updates(); // Disable anything in the {system} table that is not compatible with the current version of Drupal core. _drush_log_update_sql(update_fix_compatibility()); } function update_main() { update_main_prepare(); list($pending, $start) = updatedb_status(); // Print a list of pending updates for this module and get confirmation. if ($pending) { // @todo get table header working // array_unshift($pending, array(dt('Module'), dt('ID'), dt('Description'))); drush_print_table($pending, FALSE); if (!drush_confirm(dt('Do you wish to run all pending updates?'))) { return drush_user_abort(); } // Proceed with running all pending updates. $operations = array(); foreach ($start as $module => $version) { drupal_set_installed_schema_version($module, $version - 1); $updates = drupal_get_schema_versions($module); $max_version = max($updates); if ($version <= $max_version) { drush_log(dt('Updating module @module from schema version @start to schema version @max', array('@module' => $module, '@start' => $version - 1, '@max' => $max_version))); foreach ($updates as $update) { if ($update >= $version) { $operations[] = array('_update_do_one', array($module, $update)); } } } else { drush_log(dt('No database updates for module @module', array('@module' => $module)), LogLevel::SUCCESS); } } $batch = array( 'operations' => $operations, 'title' => 'Updating', 'init_message' => 'Starting updates', 'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.', 'finished' => 'update_finished', ); batch_set($batch); $batch =& batch_get(); $batch['progressive'] = FALSE; drush_backend_batch_process('updatedb-batch-process'); } else { drush_log(dt("No database updates required"), LogLevel::SUCCESS); } return count($pending); } /** * A simplified version of the batch_do_one function from update.php * * This does not mess with sessions and the like, as it will be used * from the command line */ function _update_do_one($module, $number, &$context) { // If updates for this module have been aborted // in a previous step, go no further. if (!empty($context['results'][$module]['#abort'])) { return; } $function = $module .'_update_'. $number; drush_log("Executing $function", LogLevel::SUCCESS); if (function_exists($function)) { $ret = $function($context['sandbox']); $context['results'][$module] = $ret; _drush_log_update_sql($ret); } if (isset($ret['#finished'])) { $context['finished'] = $ret['#finished']; unset($ret['#finished']); } if ($context['finished'] == 1 && empty($context['results'][$module]['#abort'])) { drupal_set_installed_schema_version($module, $number); } } function _update_batch_command($id) { update_main_prepare(); drush_batch_command($id); } /** * Return a 2 item array with * - an array where each item is a 3 item associative array describing a pending update. * - an array listing the first update to run, keyed by module. */ function updatedb_status() { $return = array(); $modules = drupal_get_installed_schema_version(NULL, FALSE, TRUE); foreach ($modules as $module => $schema_version) { $updates = drupal_get_schema_versions($module); // Skip incompatible module updates completely, otherwise test schema versions. if (!update_check_incompatibility($module) && $updates !== FALSE && $schema_version >= 0) { // module_invoke returns NULL for nonexisting hooks, so if no updates // are removed, it will == 0. $last_removed = module_invoke($module, 'update_last_removed'); if ($schema_version < $last_removed) { drush_set_error('PROVISION_DRUPAL_UPDATE_FAILED', dt( $module .' module can not be updated. Its schema version is '. $schema_version .'. Updates up to and including '. $last_removed .' have been removed in this release. In order to update '. $module .' module, you will first need to upgrade to the last version in which these updates were available.')); continue; } $updates = drupal_map_assoc($updates); // Record the starting update number for each module. foreach (array_keys($updates) as $update) { if ($update > $schema_version) { $start[$module] = $update; break; } } if (isset($start['system'])) { // Ensure system module's updates run first. $start = array('system' => $start['system']) + $start; } // Record any pending updates. Used for confirmation prompt. foreach (array_keys($updates) as $update) { if ($update > $schema_version) { if (class_exists('ReflectionFunction')) { // The description for an update comes from its Doxygen. $func = new ReflectionFunction($module. '_update_'. $update); $description = trim(str_replace(array("\n", '*', '/'), '', $func->getDocComment())); } if (empty($description)) { $description = dt('description not available'); } $return[] = array('module' => ucfirst($module), 'update_id' => $update, 'description' => $description); } } } } return array($return, $start); } FALSE, 'query' => 'What went wrong'); * The schema version will not be updated in this case, and all the * aborted updates will continue to appear on update.php as updates that * have not yet been run. * * @param $module * The module whose update will be run. * @param $number * The update number to run. * @param $context * The batch context array */ function drush_update_do_one($module, $number, $dependency_map, &$context) { $function = $module . '_update_' . $number; // If this update was aborted in a previous step, or has a dependency that // was aborted in a previous step, go no further. if (!empty($context['results']['#abort']) && array_intersect($context['results']['#abort'], array_merge($dependency_map, array($function)))) { return; } $context['log'] = FALSE; $ret = array(); if (function_exists($function)) { try { if ($context['log']) { Database::startLog($function); } drush_log("Executing " . $function); $ret['results']['query'] = $function($context['sandbox']); // If the update hook returned a status message (common in batch updates), // show it to the user. if ($ret['results']['query']) { drush_log($ret['results']['query'], LogLevel::OK); } $ret['results']['success'] = TRUE; } // @TODO We may want to do different error handling for different exception // types, but for now we'll just print the message. catch (Exception $e) { $ret['#abort'] = array('success' => FALSE, 'query' => $e->getMessage()); drush_set_error('DRUPAL_EXCEPTION', $e->getMessage()); } if ($context['log']) { $ret['queries'] = Database::getLog($function); } } if (isset($context['sandbox']['#finished'])) { $context['finished'] = $context['sandbox']['#finished']; unset($context['sandbox']['#finished']); } if (!isset($context['results'][$module])) { $context['results'][$module] = array(); } if (!isset($context['results'][$module][$number])) { $context['results'][$module][$number] = array(); } $context['results'][$module][$number] = array_merge($context['results'][$module][$number], $ret); if (!empty($ret['#abort'])) { // Record this function in the list of updates that were aborted. $context['results']['#abort'][] = $function; } // Record the schema update if it was completed successfully. if ($context['finished'] == 1 && empty($ret['#abort'])) { drupal_set_installed_schema_version($module, $number); } $context['message'] = 'Performed update: ' . $function; } /** * Check update requirements and report any errors. */ function update_check_requirements() { $warnings = FALSE; // Check the system module and update.php requirements only. $requirements = system_requirements('update'); $requirements += update_extra_requirements(); // If there are issues, report them. foreach ($requirements as $requirement) { if (isset($requirement['severity']) && $requirement['severity'] > REQUIREMENT_OK) { $message = isset($requirement['description']) ? $requirement['description'] : ''; if (isset($requirement['value']) && $requirement['value']) { $message .= ' (Currently using ' . $requirement['title'] . ' ' . $requirement['value'] . ')'; } $warnings = TRUE; drupal_set_message($message, LogLevel::WARNING); } } return $warnings; } function update_main_prepare() { // Some unavoidable errors happen because the database is not yet up-to-date. // Our custom error handler is not yet installed, so we just suppress them. drush_errors_off(); // We prepare a minimal bootstrap for the update requirements check to avoid // reaching the PHP memory limit. $core = DRUSH_DRUPAL_CORE; require_once $core . '/includes/bootstrap.inc'; require_once $core . '/includes/common.inc'; require_once $core . '/includes/file.inc'; require_once $core . '/includes/entity.inc'; include_once $core . '/includes/unicode.inc'; update_prepare_d7_bootstrap(); drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION); require_once $core . '/includes/install.inc'; require_once $core . '/modules/system/system.install'; // Load module basics. include_once $core . '/includes/module.inc'; $module_list['system']['filename'] = 'modules/system/system.module'; module_list(TRUE, FALSE, FALSE, $module_list); drupal_load('module', 'system'); // Reset the module_implements() cache so that any new hook implementations // in updated code are picked up. module_implements('', FALSE, TRUE); // Set up $language, since the installer components require it. drupal_language_initialize(); // Set up theme system for the maintenance page. drupal_maintenance_theme(); // Check the update requirements for Drupal. update_check_requirements(); // update_fix_d7_requirements() needs to run before bootstrapping beyond path. // So bootstrap to DRUPAL_BOOTSTRAP_LANGUAGE then include unicode.inc. drupal_bootstrap(DRUPAL_BOOTSTRAP_LANGUAGE); update_fix_d7_requirements(); // Clear the module_implements() cache before the full bootstrap. The calls // above to drupal_maintenance_theme() and update_check_requirements() have // invoked hooks before all modules have actually been loaded by the full // bootstrap. This means that the module_implements() results for any hooks // that have been invoked, including hook_module_implements_alter(), is a // smaller set of modules than should be returned normally. // @see https://github.com/drush-ops/drush/pull/399 module_implements('', FALSE, TRUE); // Now proceed with a full bootstrap. drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_FULL); drupal_maintenance_theme(); drush_errors_on(); include_once DRUPAL_ROOT . '/includes/batch.inc'; drupal_load_updates(); update_fix_compatibility(); // Change query-strings on css/js files to enforce reload for all users. _drupal_flush_css_js(); // Flush the cache of all data for the update status module. if (db_table_exists('cache_update')) { cache_clear_all('*', 'cache_update', TRUE); } module_list(TRUE, FALSE, TRUE); } function update_main() { update_main_prepare(); list($pending, $start) = updatedb_status(); if ($pending) { // @todo get table header working. // $headers = array(dt('Module'), dt('ID'), dt('Description')); drush_print_table($pending); if (!drush_confirm(dt('Do you wish to run all pending updates?'))) { return drush_user_abort(); } drush_update_batch($start); } else { drush_log(dt("No database updates required"), LogLevel::SUCCESS); } return count($pending); } function _update_batch_command($id) { update_main_prepare(); drush_batch_command($id); } /** * Start the database update batch process. * * @param $start * An array of all the modules and which update to start at. * @param $redirect * Path to redirect to when the batch has finished processing. * @param $url * URL of the batch processing page (should only be used for separate * scripts like update.php). * @param $batch * Optional parameters to pass into the batch API. * @param $redirect_callback * (optional) Specify a function to be called to redirect to the progressive * processing page. */ function drush_update_batch($start) { // Resolve any update dependencies to determine the actual updates that will // be run and the order they will be run in. $updates = update_resolve_dependencies($start); // Store the dependencies for each update function in an array which the // batch API can pass in to the batch operation each time it is called. (We // do not store the entire update dependency array here because it is // potentially very large.) $dependency_map = array(); foreach ($updates as $function => $update) { $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array(); } $operations = array(); foreach ($updates as $update) { if ($update['allowed']) { // Set the installed version of each module so updates will start at the // correct place. (The updates are already sorted, so we can simply base // this on the first one we come across in the above foreach loop.) if (isset($start[$update['module']])) { drupal_set_installed_schema_version($update['module'], $update['number'] - 1); unset($start[$update['module']]); } // Add this update function to the batch. $function = $update['module'] . '_update_' . $update['number']; $operations[] = array('drush_update_do_one', array($update['module'], $update['number'], $dependency_map[$function])); } } $batch['operations'] = $operations; $batch += array( 'title' => 'Updating', 'init_message' => 'Starting updates', 'error_message' => 'An unrecoverable error has occurred. You can find the error message below. It is advised to copy it to the clipboard for reference.', 'finished' => 'drush_update_finished', 'file' => 'includes/update.inc', ); batch_set($batch); drush_backend_batch_process('updatedb-batch-process'); } function drush_update_finished($success, $results, $operations) { // Nothing to do here. All caches already cleared. Kept as documentation of 'finished' callback. } /** * Return a 2 item array with * - an array where each item is a 3 item associative array describing a pending update. * - an array listing the first update to run, keyed by module. */ function updatedb_status() { $pending = update_get_update_list(); $return = array(); // Ensure system module's updates run first. $start['system'] = array(); // Print a list of pending updates for this module and get confirmation. foreach ($pending as $module => $updates) { if (isset($updates['start'])) { foreach ($updates['pending'] as $update_id => $description) { // Strip cruft from front. $description = str_replace($update_id . ' - ', '', $description); $return[] = array('module' => ucfirst($module), 'update_id' => $update_id, 'description' => $description); } if (isset($updates['start'])) { $start[$module] = $updates['start']; } } } return array($return, $start); } 'Create fields and instances. Returns urls for field editing.', 'core' => array('7+'), 'arguments' => array( 'bundle' => 'Content type (for nodes). Name of bundle to attach fields to. Required.', 'field_spec' => 'Comma delimited triple in the form: field_name,field_type,widget_name. If widget_name is omitted, the default widget will be used. Separate multiple fields by space. If omitted, a wizard will prompt you.' ), 'required-arguments' => 1, 'options' => array( 'entity_type' => 'Type of entity (e.g. node, user, comment). Defaults to node.', ), 'examples' => array( 'drush field-create article' => 'Define new article fields via interactive prompts.', 'open `drush field-create article`' => 'Define new article fields and then open field edit form for refinement.', 'drush field-create article city,text,text_textfield subtitle,text,text_textfield' => 'Create two new fields.' ), ); $items['field-update'] = array( 'description' => 'Return URL for field editing web page.', 'core' => array('7+'), 'arguments' => array( 'field_name' => 'Name of field that needs updating.', ), 'examples' => array( 'field-update comment_body' => 'Quickly navigate to a field edit web page.', ), ); $items['field-delete'] = array( 'description' => 'Delete a field and its instances.', 'core' => array('7+'), 'arguments' => array( 'field_name' => 'Name of field to delete.', ), 'options' => array( 'bundle' => 'Only delete the instance attached to this bundle. If omitted, admin can choose to delete one instance or whole field.', 'entity_type' => 'Disambiguate a particular bundle from identically named bundles. Usually not needed.' ), 'examples' => array( 'field-delete city' => 'Delete the city field and any instances it might have.', 'field-delete city --bundle=article' => 'Delete the city instance on the article bundle', ), ); $items['field-clone'] = array( 'description' => 'Clone a field and all its instances.', 'core' => array('7+'), 'arguments' => array( 'source_field_name' => 'Name of field that will be cloned', 'target_field_name' => 'Name of new, cloned field.', ), 'examples' => array( 'field-clone tags labels' => 'Copy \'tags\' field into a new field \'labels\' field which has same instances.', 'open `field-clone tags labels`' => 'Clone field and then open field edit forms for refinement.', ), ); $items['field-info'] = array( 'description' => 'View information about fields, field_types, and widgets.', 'core' => array('7+'), 'arguments' => array( 'type' => 'Recognized values: fields, types. If omitted, a choice list appears.', ), 'examples' => array( 'field-info types' => 'Show a table which lists all field types and their available widgets', ), 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'csv', 'field-labels' => array( 'field-name' => 'Field name', 'type' => 'Field type', 'bundle' => 'Field bundle', 'type-name' => 'Type name', 'widget' => 'Default widget', 'widgets' => 'Widgets', ), 'table-metadata' => array( 'process-cell' => '_drush_field_info_process_cell', ), 'output-data-type' => 'format-table', ), ); return $items; } /** * Command argument complete callback. */ function field_field_create_complete() { if (drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { $all = array(); $info = field_info_bundles(); foreach ($info as $entity_type => $bundles) { $all = array_merge($all, array_keys($bundles)); } return array('values' => array_unique($bundles)); } } /** * Command argument complete callback. */ function field_field_update_complete() { return field_field_complete_field_names(); } /** * Command argument complete callback. */ function field_field_delete_complete() { return field_field_complete_field_names(); } /** * Command argument complete callback. */ function field_field_clone_complete() { return field_field_complete_field_names(); } /** * Command argument complete callback. */ function field_field_info_complete() { return array('values' => array('fields', 'types')); } /** * List field names for completion. * * @return * Array of available site aliases. */ function field_field_complete_field_names() { if (drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { $info = field_info_fields(); return array('values' => array_keys($info)); } } function drush_field_create($bundle) { $entity_type = drush_get_option('entity_type', 'node'); $args = func_get_args(); array_shift($args); if (empty($args)) { // Just one item in this array for now. $args[] = drush_field_create_wizard(); } // Iterate over each field spec. foreach ($args as $string) { list($name, $type, $widget) = explode(',', $string); $info = field_info_field($name); if (empty($info)) { // Field does not exist already. Create it. $field = array( 'field_name' => $name, 'type' => $type, ); drush_op('field_create_field', $field); } // Create the instance. $instance = array( 'field_name' => $name, 'entity_type' => $entity_type, 'bundle' => $bundle, ); if ($widget) { $instance['widget'] = array('type' => $widget); } drush_op('field_create_instance', $instance); $urls[] = drush_url(drush_field_ui_bundle_admin_path($entity_type, $bundle) . '/fields/' . $name, array('absolute' => TRUE)); } drush_print(implode(' ', $urls)); } // Copy of function _field_ui_bundle_admin_path() since we don't want to load UI module. function drush_field_ui_bundle_admin_path($entity_type, $bundle_name) { $bundles = field_info_bundles($entity_type); $bundle_info = $bundles[$bundle_name]; if (isset($bundle_info['admin'])) { return isset($bundle_info['admin']['real path']) ? $bundle_info['admin']['real path'] : $bundle_info['admin']['path']; } } function drush_field_update($field_name) { $info = field_info_field($field_name); foreach ($info['bundles'] as $entity_type => $bundles) { foreach ($bundles as $bundle) { $urls[] = drush_url(drush_field_ui_bundle_admin_path($entity_type, $bundle) . '/fields/' . $field_name, array('absolute' => TRUE)); } } drush_print(implode(' ', $urls)); } function drush_field_delete($field_name) { $info = field_info_field($field_name); $confirm = TRUE; if (!$bundle = drush_get_option('bundle')) { foreach ($info['bundles'] as $entity_type => $bundles) { foreach ($bundles as $bundle) { $all_bundles[] = $bundle; } } if (count($all_bundles) > 1) { $options = array_merge(array('all' => dt('All bundles')), array_combine($all_bundles, $all_bundles)); $bundle = drush_choice($options, dt("Choose a particular bundle or 'All bundles'")); if (!$bundle) { return drush_user_abort(); } $confirm = FALSE; } else { if (!drush_confirm(dt('Do you want to delete the !field_name field?', array('!field_name' => $field_name)))) { return drush_user_abort(); } } } if ($bundle == 'all') { foreach ($info['bundles'] as $entity_type => $bundles) { foreach ($bundles as $bundle) { $instance = field_info_instance($entity_type, $field_name, $bundle); drush_op('field_delete_instance', $instance); } } } else { $entity_type = drush_field_get_entity_from_bundle($bundle); $instance = field_info_instance($entity_type, $field_name, $bundle); drush_op('field_delete_instance', $instance); } // If there are no more bundles, delete the field. $info = field_info_field($field_name); if (empty($info['bundles'])) { drush_op('field_delete_field', $field_name); } } function drush_field_clone($source_field_name, $target_field_name) { if (!$info = field_info_field($source_field_name)) { return drush_set_error(dt('!source not found in field list.', array('!source' => $source_field_name))); } unset($info['id']); $info['field_name'] = $target_field_name; $target = drush_op('field_create_field', $info); foreach ($info['bundles'] as $entity_type => $bundles) { foreach ($bundles as $bundle) { $instance = field_info_instance($entity_type, $source_field_name, $bundle); $instance['field_name'] = $target_field_name; unset($instance['id']); $instance['field_id'] = $target['id']; drush_op('field_create_instance', $instance); $urls[] = drush_url(drush_field_ui_bundle_admin_path($entity_type, $bundle) . '/fields/' . $target_field_name, array('absolute' => TRUE)); } } drush_print(implode(' ', $urls)); } function drush_field_info($type = NULL) { if (!isset($type)) { // Don't ask in 'pipe' mode -- just default to 'fields'. if (drush_get_context('DRUSH_PIPE')) { $type = 'fields'; } else { $type = drush_choice(array_combine(array('types', 'fields'), array('types', 'fields')), dt('Which information do you wish to see?')); } } $result = array(); switch ($type) { case 'fields': drush_hide_output_fields(array('type-name', 'widget', 'widgets')); $info = field_info_fields(); foreach ($info as $field_name => $field) { $bundle_strs = array(); foreach ($field['bundles'] as $entity_type => $bundles) { $bundle_strs += $bundles; } $result[$field_name] = array( 'field-name' => $field_name, 'type' => $field['type'], 'bundle' => $bundle_strs, ); } break; case 'types': drush_hide_output_fields(array('field-name', 'type', 'bundle')); $info = field_info_field_types(); module_load_include('inc', 'field_ui', 'field_ui.admin'); $widgets = field_info_widget_types(); foreach ($info as $type_name => $type) { $widgets = field_ui_widget_type_options($type_name); $result[$type_name] = array( 'type-name' => $type_name, 'widget' => $type['default_widget'], 'widgets' => $widgets, ); } break; default: return drush_set_error('DRUSH_FIELD_INVALID_SELECTION', dt("Argument for drush field-info must be 'fields' or 'types'")); } return $result; } /** * We need to handle the formatting of cells in table-format * output specially. In 'types' output, the output data is a simple * associative array of machine names => human-readable names. * We choose to show the machine names. In 'fields' output, the * output data is a list of entity types, each of which contains a list * of bundles. We comma-separate the bundles, and space-separate * the entities. */ function _drush_field_info_process_cell($data, $metadata) { $first = reset($data); if (is_array($first)) { foreach($data as $entity => $bundles) { $list[] = drush_format($bundles, array(), 'csv'); } return drush_format($list, array(), 'list'); } return drush_format(array_keys($data), array(), 'csv'); } /** * Prompt user enough to create basic field and instance. * * @return array $field_spec * An array of brief field specifications. */ function drush_field_create_wizard() { $specs[] = drush_prompt(dt('Field name')); module_load_include('inc', 'field_ui', 'field_ui.admin'); $types = field_ui_field_type_options(); $field_type = drush_choice($types, dt('Choose a field type')); $specs[] = $field_type; $widgets = field_ui_widget_type_options($field_type); $specs[] = drush_choice($widgets, dt('Choose a widget')); return implode(',', $specs); } function drush_field_get_entity_from_bundle($bundle) { if (drush_get_option('entity_type')) { return drush_get_option('entity_type'); } else { $info = field_info_bundles(); foreach ($info as $entity_type => $bundles) { if (isset($bundles[$bundle])) { return $entity_type; } } } } ' * * @param * A string with the help section (prepend with 'drush:') * * @return * A string with the help text for your command. */ function help_drush_help($section) { switch ($section) { case 'drush:help': return dt("Drush provides an extensive help system that describes both drush commands and topics of general interest. Use `drush help --filter` to present a list of command categories to view, and `drush topic` for a list of topics that go more in-depth on how to use and extend drush."); } } /** * Implementation of hook_drush_command(). * * In this hook, you specify which commands your * drush module makes available, what it does and * description. * * Notice how this structure closely resembles how * you define menu hooks. * * @return * An associative array describing your command(s). */ function help_drush_command() { $items = array(); $items['help'] = array( 'description' => 'Print this help message. See `drush help help` for more options.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'allow-additional-options' => array('helpsingle'), 'options' => array( 'sort' => 'Sort commands in alphabetical order. Drush waits for full bootstrap before printing any commands when this option is used.', 'filter' => array( 'description' => 'Restrict command list to those commands defined in the specified file. Omit value to choose from a list of names.', 'example-value' => 'category', 'value' => 'optional', ), ), 'arguments' => array( 'command' => 'A command name, or command alias.', ), 'examples' => array( 'drush' => 'List all commands.', 'drush --filter=devel_generate' => 'Show only commands defined in devel_generate.drush.inc', 'drush help pm-download' => 'Show help for one command.', 'drush help dl' => 'Show help for one command using an alias.', 'drush help --format=html' => 'Show an HTML page detailing all available commands.', 'drush help --format=json' => 'All available comamnds, in a machine parseable format.', ), // Use output format system for all formats except the default presentation. 'outputformat' => array( 'default' => 'table', 'field-labels' => array('name' => 'Name', 'description' => 'Description'), 'output-data-type' => 'format-table', ), 'topics' => array('docs-readme'), ); return $items; } /** * Command argument complete callback. * * For now, this can't move to helpsingle since help command is the entry point for both. * * @return * Array of available command names. */ function core_help_complete() { return array('values' => array_keys(drush_get_commands())); } /** * Command callback for help command. This is the default command, when none * other has been specified. */ function drush_core_help($name = '') { $format = drush_get_option('format', 'table'); if ($name) { // helpsingle command builds output when a command is specified. $options = drush_redispatch_get_options(); if ($name != 'help') { unset($options['help']); } $return = drush_invoke_process('@self' ,'helpsingle', func_get_args(), $options); drush_backend_set_result($return['object']); return; } // For speed, only bootstrap up to DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION. drush_bootstrap_max(); drush_get_commands(true); $implemented = drush_get_commands(); ksort($implemented); $command_categories = drush_commands_categorize($implemented); if ($format != 'table') { return $command_categories; } else { $visible = drush_help_visible($command_categories); // If the user specified --filter w/out a value, then // present a choice list of help categories. if (drush_get_option('filter', FALSE) === TRUE) { $help_categories = array(); foreach ($command_categories as $key => $info) { $description = $info['title']; if (array_key_exists('summary', $info)) { $description .= ": " . $info['summary']; } $help_categories[$key] = $description; } $result = drush_choice($help_categories, 'Select a help category:'); if (!$result) { return drush_user_abort(); } drush_set_option('filter', $result); } // Filter out categories that the user does not want to see $filter_category = drush_get_option('filter'); if (!empty($filter_category) && ($filter_category !== TRUE)) { if (!array_key_exists($filter_category, $command_categories)) { return drush_set_error('DRUSH_NO_CATEGORY', dt("The specified command category !filter does not exist.", array('!filter' => $filter_category))); } $command_categories = array($filter_category => $command_categories[$filter_category]); } // Make a fake command section to hold the global options, then print it. $global_options_help = drush_global_options_command(TRUE); if (!drush_get_option('filter')) { drush_print_help($global_options_help); } drush_help_listing_print($command_categories); drush_backend_set_result($command_categories); return; } } // Uncategorize the list of commands. Hiddens have been removed and // filtering performed. function drush_help_visible($command_categories) { $all = array(); foreach ($command_categories as $category => $info) { $all = array_merge($all, $info['commands']); } return $all; } /** * Print CLI table listing all commands. */ function drush_help_listing_print($command_categories) { $all_commands = array(); foreach ($command_categories as $key => $info) { // Get the commands in this category. $commands = $info['commands']; // Build rows for drush_print_table(). $rows = array(); foreach($commands as $cmd => $command) { $name = $command['aliases'] ? $cmd . ' (' . implode(', ', $command['aliases']) . ')': $cmd; $rows[$cmd] = array('name' => $name, 'description' => $command['description']); } drush_print($info['title'] . ": (" . $key . ")"); drush_print_table($rows, FALSE, array('name' => 20)); } } /** * Build a fake command for the purposes of showing examples and options. */ function drush_global_options_command($brief = FALSE) { $global_options_help = array( 'description' => 'Execute a drush command. Run `drush help [command]` to view command-specific help. Run `drush topic` to read even more documentation.', 'sections' => array( 'options' => 'Global options (see `drush topic core-global-options` for the full list)', ), 'options' => drush_get_global_options($brief), 'examples' => array( 'drush dl cck zen' => 'Download CCK module and Zen theme.', 'drush --uri=http://example.com status' => 'Show status command for the example.com multi-site.', ), '#brief' => TRUE, ); $global_options_help += drush_command_defaults('global-options', 'global_options', __FILE__); drush_command_invoke_all_ref('drush_help_alter', $global_options_help); ksort($global_options_help['options']); return $global_options_help; } 'Print help for a single command', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'allow-additional-options' => TRUE, 'hidden' => TRUE, 'arguments' => array( 'command' => 'A command name, or command alias.', ), 'examples' => array( 'drush help pm-download' => 'Show help for one command.', 'drush help dl' => 'Show help for one command using an alias.', ), 'topics' => array('docs-readme'), ); return $items; } /** * Command callback. Show help for a single command. */ function drush_core_helpsingle($commandstring) { // First check and see if the command can already be found. $commands = drush_get_commands(); if (!array_key_exists($commandstring, $commands)) { // If the command cannot be found, then bootstrap so that // additional commands will be brought in. // TODO: We need to do a full bootstrap in order to find module service // commands. We only need to do this for Drupal 8, though; 7 and earlier // can stop at DRUSH_BOOTSTRAP_DRUPAL_SITE. Perhaps we could use command // caching to avoid bootstrapping, if we have collected the commands for // this site once already. drush_bootstrap_max(); $commands = drush_get_commands(); } if (array_key_exists($commandstring, $commands)) { $command = $commands[$commandstring]; annotationcommand_adapter_add_hook_options($command); drush_print_help($command); return TRUE; } $shell_aliases = drush_get_context('shell-aliases', array()); if (array_key_exists($commandstring, $shell_aliases)) { $msg = dt("'@alias-name' is a shell alias. Its value is: !name. See `drush topic docs-shell-aliases` and `drush shell-alias` for more information.", array('@alias-name' => $commandstring, '!name' => $shell_aliases[$commandstring])); drush_log($msg, 'ok'); return TRUE; } return drush_set_error('DRUSH_COMMAND_NOT_FOUND', dt('Invalid command !command.', array('!command' => $commandstring))); } /** * Print the help for a single command to the screen. * * @param array $command * A fully loaded $command array. */ function drush_print_help($command) { _drush_help_merge_subcommand_information($command); if (!$help = drush_command_invoke_all('drush_help', 'drush:'. $command['command'])) { $help = array($command['description']); } if ($command['strict-option-handling']) { $command['topics'][] = 'docs-strict-options'; } // Give commandfiles an opportunity to add examples and options to the command. drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_SITE); drush_engine_add_help_topics($command); drush_command_invoke_all_ref('drush_help_alter', $command); drush_print(wordwrap(implode("\n", $help), drush_get_context('DRUSH_COLUMNS', 80))); drush_print(); $global_options = drush_get_global_options(); foreach ($command['global-options'] as $global_option) { $command['options'][$global_option] = $global_options[$global_option]; } // Sort command options. uksort($command['options'], '_drush_help_sort_command_options'); // Print command sections help. foreach ($command['sections'] as $key => $value) { if (!empty($command[$key])) { $rows = drush_format_help_section($command, $key); if ($rows) { drush_print(dt($value) . ':'); drush_print_table($rows, FALSE, array('label' => 40)); unset($rows); drush_print(); } } } // Append aliases if any. if ($command['aliases']) { drush_print(dt("Aliases: ") . implode(', ', $command['aliases'])); } } /** * Sort command options alphabetically. Engine options at the end. */ function _drush_help_sort_command_options($a, $b) { $engine_a = strpos($a, '='); $engine_b = strpos($b, '='); if ($engine_a && !$engine_b) { return 1; } else if (!$engine_a && $engine_b) { return -1; } elseif ($engine_a && $engine_b) { if (substr($a, 0, $engine_a) == substr($b, 0, $engine_b)) { return 0; } } return ($a < $b) ? -1 : 1; } /** * Check to see if the specified command contains an 'allow-additional-options' * record. If it does, find the additional options that are allowed, and * add in the help text for the options of all of the sub-commands. */ function _drush_help_merge_subcommand_information(&$command) { // 'allow-additional-options' will either be FALSE (default), // TRUE ("allow anything"), or an array that lists subcommands // that are or may be called via drush_invoke by this command. if (is_array($command['allow-additional-options'])) { $implemented = drush_get_commands(); foreach ($command['allow-additional-options'] as $subcommand_name) { if (array_key_exists($subcommand_name, $implemented)) { $command['options'] += $implemented[$subcommand_name]['options']; $command['sub-options'] = array_merge_recursive($command['sub-options'], $implemented[$subcommand_name]['sub-options']); if (empty($command['arguments'])) { $command['arguments'] = $implemented[$subcommand_name]['arguments']; } $command['topics'] = array_merge($command['topics'], $implemented[$subcommand_name]['topics']); } } } } /** * Format one named help section from a command record * * @param $command * A command record with help information * @param $section * The name of the section to format ('options', 'topic', etc.) * @returns array * Formatted rows, suitable for printing via drush_print_table. The returned * array can be empty. */ function drush_format_help_section($command, $section) { $rows = array(); $formatter = (function_exists('drush_help_section_formatter_' . $section)) ? 'drush_help_section_formatter_' . $section : 'drush_help_section_default_formatter'; foreach ($command[$section] as $name => $help_attributes) { if (!is_array($help_attributes)) { $help_attributes = array('description' => $help_attributes); } $help_attributes['label'] = $name; call_user_func_array($formatter, array($command, &$help_attributes)); if (empty($help_attributes['hidden'])) { $rows[] = array('label' => $help_attributes['label'], 'description' => $help_attributes['description']); // Process the subsections too, if any if (!empty($command['sub-' . $section]) && array_key_exists($name, $command['sub-' . $section])) { $rows = array_merge($rows, _drush_format_help_subsection($command, $section, $name, $formatter)); } } } return $rows; } /** * Format one named portion of a subsection from a command record. * Subsections allow related parts of a help record to be grouped * together. For example, in the 'options' section, sub-options that * are related to a particular primary option are stored in a 'sub-options' * section whose name == the name of the primary option. * * @param $command * A command record with help information * @param $section * The name of the section to format ('options', 'topic', etc.) * @param $subsection * The name of the subsection (e.g. the name of the primary option) * @param $formatter * The name of a function to use to format the rows of the subsection * @param $prefix * Characters to prefix to the front of the label (for indentation) * @returns array * Formatted rows, suitable for printing via drush_print_table. */ function _drush_format_help_subsection($command, $section, $subsection, $formatter, $prefix = ' ') { $rows = array(); foreach ($command['sub-' . $section][$subsection] as $name => $help_attributes) { if (!is_array($help_attributes)) { $help_attributes = array('description' => $help_attributes); } $help_attributes['label'] = $name; call_user_func_array($formatter, array($command, &$help_attributes)); if (!array_key_exists('hidden', $help_attributes)) { $rows[] = array('label' => $prefix . $help_attributes['label'], 'description' => $help_attributes['description']); // Process the subsections too, if any if (!empty($command['sub-' . $section]) && array_key_exists($name, $command['sub-' . $section])) { $rows = array_merge($rows, _drush_format_help_subsection($command, $section, $name, $formatter, $prefix . ' ')); } } } return $rows; } /** * The options section formatter. Adds a "--" in front of each * item label. Also handles short-form and example-value * components in the help attributes. */ function drush_help_section_formatter_options($command, &$help_attributes) { if ($help_attributes['label'][0] == '-') { drush_log(dt("Option '!option' of command !command should instead be declared as '!fixed'", array('!option' => $help_attributes['label'], '!command' => $command['command'], '!fixed' => preg_replace('/^--*/', '', $help_attributes['label']))), 'debug'); } else { $help_attributes['label'] = '--' . $help_attributes['label']; } if (!empty($help_attributes['required'])) { $help_attributes['description'] .= " " . dt("Required."); } $prefix = '<'; $suffix = '>'; if (array_key_exists('example-value', $help_attributes)) { if (isset($help_attributes['value']) && $help_attributes['value'] == 'optional') { $prefix = '['; $suffix = ']'; } $help_attributes['label'] .= '=' . $prefix . $help_attributes['example-value'] . $suffix; if (array_key_exists('short-form', $help_attributes)) { $help_attributes['short-form'] .= " $prefix" . $help_attributes['example-value'] . $suffix; } } if (array_key_exists('short-form', $help_attributes)) { $help_attributes['label'] = '-' . $help_attributes['short-form'] . ', ' . $help_attributes['label']; } drush_help_section_default_formatter($command, $help_attributes); } /** * The default section formatter. Replaces '[command]' with the * command name. */ function drush_help_section_default_formatter($command, &$help_attributes) { // '[command]' is a token representing the current command. @see pm_drush_engine_version_control(). $help_attributes['label'] = str_replace('[command]', $command['command'], $help_attributes['label']); } 'Flush all derived images for a given style.', 'core' => array('7+'), 'arguments' => array( 'style' => 'An image style machine name. If not provided, user may choose from a list of names.', ), 'options' => array( 'all' => 'Flush all derived images', ), 'examples' => array( 'drush image-flush' => 'Pick an image style and then delete its images.', 'drush image-flush thumbnail' => 'Delete all thumbnail images.', 'drush image-flush --all' => 'Flush all derived images. They will be regenerated on the fly.', ), 'aliases' => array('if'), ); $items['image-derive'] = array( 'description' => 'Create an image derivative.', 'core' => array('7+'), 'drupal dependencies' => array('image'), 'arguments' => array( 'style' => 'An image style machine name.', 'source' => 'Path to a source image. Optionally prepend stream wrapper scheme.', ), 'required arguments' => TRUE, 'options' => array(), 'examples' => array( 'drush image-derive thumbnail themes/bartik/logo.png' => 'Save thumbnail sized derivative of logo image.', ), 'aliases' => array('id'), ); return $items; } /** * Implements hook_drush_help_alter(). */ function image_drush_help_alter(&$command) { // Drupal 8+ customizations. if ($command['command'] == 'image-derive' && drush_drupal_major_version() >= 8) { unset($command['examples']); $command['examples']['drush image-derive thumbnail core/themes/bartik/logo.png'] = 'Save thumbnail sized derivative of logo image.'; } } /** * Command argument complete callback. * * @return * Array of available configuration files for editing. */ function image_image_flush_complete() { drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_FULL); drush_include_engine('drupal', 'image'); return array('values' => array_keys(drush_image_styles())); } function drush_image_flush_pre_validate($style_name = NULL) { drush_include_engine('drupal', 'image'); if (!empty($style_name) && !$style = drush_image_style_load($style_name)) { return drush_set_error(dt('Image style !style not recognized.', array('!style' => $style_name))); } } function drush_image_flush($style_name = NULL) { drush_include_engine('drupal', 'image'); if (drush_get_option('all')) { $style_name = 'all'; } if (empty($style_name)) { $styles = array_keys(drush_image_styles()); $choices = array_combine($styles, $styles); $choices = array_merge(array('all' => 'all'), $choices); $style_name = drush_choice($choices, dt("Choose a style to flush.")); if ($style_name === FALSE) { return drush_user_abort(); } } if ($style_name == 'all') { foreach (drush_image_styles() as $style_name => $style) { drush_image_flush_single($style_name); } drush_log(dt('All image styles flushed'), LogLevel::SUCCESS); } else { drush_image_flush_single($style_name); } } function drush_image_derive_validate($style_name, $source) { drush_include_engine('drupal', 'image'); if (!$style = drush_image_style_load($style_name)) { return drush_set_error(dt('Image style !style not recognized.', array('!style' => $style_name))); } if (!file_exists($source)) { return drush_set_error(dt('Source file not found - !file.', array('!file' => $source))); } } /* * Command callback. Create an image derivative. * * @param string $style_name * The name of an image style. * * @param string $source * The path to a source image, relative to Drupal root. */ function drush_image_derive($style_name, $source) { drush_include_engine('drupal', 'image'); return _drush_image_derive($style_name, $source); } 'Enrich the bash startup file with completion and aliases. Copy .drushrc file to ~/.drush', 'aliases' => array('init'), 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'package' => 'core', 'global-options' => array('editor', 'bg'), 'options' => array( 'edit' => 'Open the new config file in an editor.', 'add-path' => "Always add Drush to the \$PATH in the user's .bashrc file, even if it is already in the \$PATH. Use --no-add-path to skip updating .bashrc with the Drush \$PATH. Default is to update .bashrc only if Drush is not already in the \$PATH.", ), 'examples' => array( 'drush core-init --edit' => 'Enrich Bash and open drush config file in editor.', 'drush core-init --edit --bg' => 'Return to shell prompt as soon as the editor window opens.', ), ); return $items; } /** * Initialize local Drush configuration */ function drush_init_core_init() { $home = drush_server_home(); $drush_config_dir = $home . "/.drush"; $drush_config_file = $drush_config_dir . "/drushrc.php"; $drush_bashrc = $drush_config_dir . "/drush.bashrc"; $drush_prompt = $drush_config_dir . "/drush.prompt.sh"; $drush_complete = $drush_config_dir . "/drush.complete.sh"; $examples_dir = DRUSH_BASE_PATH . "/examples"; $example_configuration = $examples_dir . "/example.drushrc.php"; $example_bashrc = $examples_dir . "/example.bashrc"; $example_prompt = $examples_dir . "/example.prompt.sh"; $example_complete = DRUSH_BASE_PATH . "/drush.complete.sh"; $bashrc_additions = array(); // Create a ~/.drush directory if it does not yet exist if (!is_dir($drush_config_dir)) { drush_mkdir($drush_config_dir); } // If there is no ~/.drush/drushrc.php, then copy the // example Drush configuration file here if (!is_file($drush_config_file)) { copy($example_configuration, $drush_config_file); drush_log(dt("Copied example Drush configuration file to !path", array('!path' => $drush_config_file)), LogLevel::OK); } // If Drush is not in the $PATH, then figure out which // path to add so that Drush can be found globally. $add_path = drush_get_option('add-path', NULL); if ((!drush_which("drush") || $add_path) && ($add_path !== FALSE)) { $drush_path = drush_find_path_to_drush($home); $drush_path = preg_replace("%^" . preg_quote($home) . "/%", '$HOME/', $drush_path); $bashrc_additions["%$drush_path%"] = "\n# Path to Drush, added by 'drush init'.\nexport PATH=\"\$PATH:$drush_path\"\n\n"; } // If there is no ~/.drush/drush.bashrc file, then copy // the example bashrc file there if (!is_file($drush_bashrc)) { copy($example_bashrc, $drush_bashrc); $pattern = basename($drush_bashrc); $bashrc_additions["%$pattern%"] = "\n# Include Drush bash customizations.". drush_bash_addition($drush_bashrc); drush_log(dt("Copied example Drush bash configuration file to !path", array('!path' => $drush_bashrc)), LogLevel::OK); } // If there is no ~/.drush/drush.complete.sh file, then copy it there if (!is_file($drush_complete)) { copy($example_complete, $drush_complete); $pattern = basename($drush_complete); $bashrc_additions["%$pattern%"] = "\n# Include Drush completion.\n". drush_bash_addition($drush_complete); drush_log(dt("Copied Drush completion file to !path", array('!path' => $drush_complete)), LogLevel::OK); } // If there is no ~/.drush/drush.prompt.sh file, then copy // the example prompt.sh file here if (!is_file($drush_prompt)) { copy($example_prompt, $drush_prompt); $pattern = basename($drush_prompt); $bashrc_additions["%$pattern%"] = "\n# Include Drush prompt customizations.\n". drush_bash_addition($drush_prompt); drush_log(dt("Copied example Drush prompt file to !path", array('!path' => $drush_prompt)), LogLevel::OK); } // Decide whether we want to add our Bash commands to // ~/.bashrc or ~/.bash_profile $bashrc = drush_init_find_bashrc($home); // Modify the user's bashrc file, adding our customizations. $bashrc_contents = ""; if (file_exists($bashrc)) { $bashrc_contents = file_get_contents($bashrc); } $new_bashrc_contents = $bashrc_contents; foreach ($bashrc_additions as $pattern => $addition) { // Only put in the addition if the pattern does not already // exist in the bashrc file. if (!preg_match($pattern, $new_bashrc_contents)) { $new_bashrc_contents = $new_bashrc_contents . $addition; } } if ($new_bashrc_contents != $bashrc_contents) { if (drush_confirm(dt(implode('', $bashrc_additions) . "Append the above code to !file?", array('!file' => $bashrc)))) { file_put_contents($bashrc, "\n\n". $new_bashrc_contents); drush_log(dt("Updated bash configuration file !path", array('!path' => $bashrc)), LogLevel::OK); drush_log(dt("Start a new shell in order to experience the improvements (e.g. `bash`)."), LogLevel::OK); if (drush_get_option('edit')) { $exec = drush_get_editor(); drush_shell_exec_interactive($exec, $drush_config_file, $drush_config_file); } } else { return drush_user_abort(); } } else { drush_log(dt('No code added to !path', array('!path' => $bashrc)), LogLevel::OK); } } /** * Determine which .bashrc file is best to use on this platform. */ function drush_init_find_bashrc($home) { return $home . "/.bashrc"; } /** * Determine where Drush is located, so that we can add * that location to the $PATH */ function drush_find_path_to_drush($home) { // First test: is Drush inside a vendor directory? // Does vendor/bin exist? If so, use that. We do // not have a good way to locate the 'bin' directory // if it has been relocated in the composer.json config // section. if ($vendor_pos = strpos(DRUSH_BASE_PATH, "/vendor/")) { $vendor_dir = substr(DRUSH_BASE_PATH, 0, $vendor_pos + 7); $vendor_bin = $vendor_dir . '/bin'; if (is_dir($vendor_bin)) { return $vendor_bin; } } // Fallback is to use the directory that Drush is in. return DRUSH_BASE_PATH; } function drush_bash_addition($file) { return << 'Checks for available translation updates.', 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, ]; $items['locale-update'] = [ 'description' => 'Updates the available translations.', 'options' => [ 'langcodes' => 'A comma-separated list of language codes to update. If omitted, all translations will be updated.' ], 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, ]; // @todo Implement proper export and import commands. return $items; } /** * Checks for available translation updates. * * @see \Drupal\locale\Controller\LocaleController::checkTranslation() * * @todo This can be simplified once https://www.drupal.org/node/2631584 lands * in Drupal core. */ function drush_locale_check() { \Drupal::moduleHandler()->loadInclude('locale', 'inc', 'locale.compare'); // Check translation status of all translatable project in all languages. // First we clear the cached list of projects. Although not strictly // necessary, this is helpful in case the project list is out of sync. locale_translation_flush_projects(); locale_translation_check_projects(); // Execute a batch if required. A batch is only used when remote files // are checked. if (batch_get()) { drush_backend_batch_process(); } } /** * Imports the available translation updates. * * @see TranslationStatusForm::buildForm() * @see TranslationStatusForm::prepareUpdateData() * @see TranslationStatusForm::submitForm() * * @todo This can be simplified once https://www.drupal.org/node/2631584 lands * in Drupal core. */ function drush_locale_update() { $module_handler = \Drupal::moduleHandler(); $module_handler->loadInclude('locale', 'fetch.inc'); $module_handler->loadInclude('locale', 'bulk.inc'); $langcodes = []; foreach (locale_translation_get_status() as $project_id => $project) { foreach ($project as $langcode => $project_info) { if (!empty($project_info->type)) { $langcodes[] = $langcode; } } } if ($passed_langcodes = drush_get_option('langcodes')) { $langcodes = array_intersect($langcodes, explode(',', $passed_langcodes)); // @todo Not selecting any language code in the user interface results in // all translations being updated, so we mimick that behavior here. } // Deduplicate the list of langcodes since each project may have added the // same language several times. $langcodes = array_unique($langcodes); // @todo Restricting by projects is not possible in the user interface and is // broken when attempting to do it in a hook_form_alter() implementation so // we do not allow for it here either. $projects = []; // Set the translation import options. This determines if existing // translations will be overwritten by imported strings. $options = _locale_translation_default_update_options(); // If the status was updated recently we can immediately start fetching the // translation updates. If the status is expired we clear it an run a batch to // update the status and then fetch the translation updates. $last_checked = \Drupal::state()->get('locale.translation_last_checked'); if ($last_checked < REQUEST_TIME - LOCALE_TRANSLATION_STATUS_TTL) { locale_translation_clear_status(); $batch = locale_translation_batch_update_build(array(), $langcodes, $options); batch_set($batch); } else { // Set a batch to download and import translations. $batch = locale_translation_batch_fetch_build($projects, $langcodes, $options); batch_set($batch); // Set a batch to update configuration as well. if ($batch = locale_config_batch_update_components($options, $langcodes)) { batch_set($batch); } } drush_backend_batch_process(); } 'Use system notifications to signal command completion. If set to a number, commands that finish in fewer seconds will not trigger a notification.', 'example-value' => 60, 'never-propagate' => TRUE, ); $command['options']['notify-audio'] = array( 'description' => 'Trigger an audio alert to signal command completion. If set to a number, commands that finish in fewer seconds will not trigger a notification.', 'example-value' => 60, 'never-propagate' => TRUE, ); $command['sub-options']['notify']['notify-cmd'] = array( 'description' => 'Specify the shell command to trigger the notification.', 'never-propagate' => TRUE, ); $command['sub-options']['notify']['notify-cmd-audio'] = array( 'description' => 'Specify the shell command to trigger the audio notification.', 'never-propagate' => TRUE, ); } } } /** * Implements hook_drush_help(). */ function notify_drush_help($section) { switch ($section) { case 'notify:cache-clear': return dt('Caches have been cleared.'); case 'notify:site-install:error': return dt('Failed on site installation'); } } /** * Shutdown function to signal on errors. */ function drush_notify_shutdown() { $cmd = drush_get_command(); if (empty($cmd['command'])) { return; } // pm-download handles its own notification. if ($cmd['command'] != 'pm-download' && drush_notify_allowed($cmd['command'])) { $msg = dt("Command '!command' completed.", array('!command' => $cmd['command'])); drush_notify_send(drush_notify_command_message($cmd['command'], $msg)); } if (drush_get_option('notify', FALSE) && drush_get_error()) { // If the only error is that notify failed, do not try to notify again. $log = drush_get_error_log(); if (count($log) == 1 && array_key_exists('NOTIFY_COMMAND_NOT_FOUND', $log)) { return; } // Send an alert that the command failed. if (drush_notify_allowed($cmd['command'])) { $msg = dt("Command '!command' failed.", array('!command' => $cmd['command'])); drush_notify_send(drush_notify_command_message($cmd['command'] . ':error', $msg)); } } } /** * Determine the message to send on command completion. * * @param string $command * Name of the Drush command for which we check message overrides. * @param string $default * (Default: NULL) Default message to use if there are not notification message overrides. * * @return string * Message to use for notification. */ function drush_notify_command_message($command, $default = NULL) { if ($msg = drush_command_invoke_all('drush_help', 'notify:' . $command)) { $msg = implode("\n", $msg); } else { $msg = $default ? $default : $msg = $command . ': No news is good news.'; } return $msg; } /** * Prepares and dispatches notifications to delivery mechanisms. * * You may avoid routing a message to secondary messaging mechanisms (e.g. audio), * by direct use of the delivery functions. * * @param string $msg * Message to send via notification. */ function drush_notify_send($msg) { drush_notify_send_text($msg); if (drush_get_option('notify-audio', FALSE)) { drush_notify_send_audio($msg); } } /** * Send text-based system notification. * * This is the automatic, default behavior. It is intended for use with tools * such as libnotify in Linux and Notification Center on OSX. * * @param string $msg * Message text for delivery. * * @return bool * TRUE on success, FALSE on failure */ function drush_notify_send_text($msg) { $override = drush_get_option('notify-cmd', FALSE); if (!empty($override)) { $cmd = $override; } else { switch (PHP_OS) { case 'Darwin': $cmd = 'terminal-notifier -message %s -title Drush'; $error_message = dt('terminal-notifier command failed. Please install it from https://github.com/alloy/terminal-notifier.'); break; case 'Linux': default: $icon = drush_normalize_path(DRUSH_BASE_PATH . '/drush_logo-black.png'); $cmd = "notify-send %s -i $icon"; $error_message = dt('notify-send command failed. Please install it as per http://coderstalk.blogspot.com/2010/02/how-to-install-notify-send-in-ubuntu.html.'); break; } } if (!drush_shell_exec($cmd, $msg)) { return drush_set_error('NOTIFY_COMMAND_NOT_FOUND', $error_message . ' ' . dt('Or you may specify an alternate command to run by specifying --notify-cmd=')); } return TRUE; } /** * Send an audio-based system notification. * * This function is only automatically invoked with the additional use of the * --notify-audio flag or configuration state. * * @param $msg * Message for audio recital. * * @return bool * TRUE on success, FALSE on failure */ function drush_notify_send_audio($msg) { $override = drush_get_option('notify-cmd-audio', FALSE); if (!empty($override)) { $cmd = $override; } else { switch (PHP_OS) { case 'Darwin': $cmd = 'say %s'; break; case 'Linux': default: $cmd = drush_get_option('notify-cmd-audio', 'spd-say') . ' %s'; } } if (!drush_shell_exec($cmd, $msg)) { return drush_set_error('NOTIFY_COMMAND_NOT_FOUND', dt('The third party notification utility failed.')); } } /** * Identify if the given Drush request should trigger a notification. * * @param $command * Name of the command. * * @return * Boolean */ function drush_notify_allowed($command) { $notify = drush_get_option(array('notify', 'notify-audio'), FALSE); $execution = time() - $_SERVER['REQUEST_TIME']; return ($notify === TRUE || (is_numeric($notify) && $notify > 0 && $execution > $notify)); } Drush help

    Global Options (see `drush topic core-global-options` for the full list)

    $row) { print ''; foreach ($row as $value) { print "\n"; } print "\n"; } ?>
    " . htmlspecialchars($value) . "

    Command list

    $command) { print " \n"; } ?>
    $key" . $command['description'] . "

    Command detail

    $command) { print "\n
    $key
    \n";
            drush_core_helpsingle($key);
            print "
    \n"; } ?> array("b" => 2, "c" => 3), * "d" => array("e" => 5, "f" => 6) * ); * * Output with --format=json: * * {"a":{"b":2,"c":3},"d":{"e":5,"f":6}} */ class drush_outputformat_json extends drush_outputformat { function format($input, $metadata) { return drush_json_encode($input); } } "Two B or ! Two B, that is the comparison", * "c" => "I see that C has gone to Sea" * ); * * Output with --format=key-value: * * b : Two B or ! Two B, * that is the * comparison * c : I see that C has gone * to Sea * * Code: * * return array( * "a" => array( * "b" => "Two B or ! Two B, that is the comparison", * "c" => "I see that C has gone to Sea" * ), * "d" => array( * "e" => "Elephants and electron microscopes", * "f" => "My margin is too small" * ) * ); * * Output with --format=key-value-list: * * b : Two B or ! Two B, * that is the * comparison * c : I see that C has gone * to Sea * * e : Elephants and * electron microscopes * f : My margin is too * small */ class drush_outputformat_key_value extends drush_outputformat { function format($input, $metadata) { if (!is_array($input)) { if (isset($metadata['label'])) { $input = array(dt($metadata['label']) => $input); } else { return $this->format_error(dt('No label provided.')); } } $kv_metadata = isset($metadata['table-metadata']) ? $metadata['table-metadata'] : array(); if ((!isset($kv_metadata['key-value-item'])) && (isset($metadata['field-labels']))) { $input = drush_select_output_fields($input, $metadata['field-labels'], $metadata['field-mappings']); } if (isset($metadata['include-field-labels'])) { $kv_metadata['include-field-labels'] = $metadata['include-field-labels']; } $formatted_table = drush_key_value_to_array_table($input, $kv_metadata); if ($formatted_table === FALSE) { return FALSE; } return drush_format_table($formatted_table, FALSE, array()); } } engine_config as $key => $value) { if ((substr($key, 0, 4) == 'list') || (substr($key, -6) == 'filter') || (substr($key, 0, 5) == 'field') || ($key == 'options')) { unset($this->engine_config[$key]); $list_metadata[$key] = $value; } } foreach ($this->engine_config['engine-info'] as $key => $value) { if ((substr($key, 0, 4) == 'list') || (substr($key, -6) == 'filter')) { unset($this->engine_config['engine-info'][$key]); $list_metadata[$key] = $value; } } $sub_formatter = isset($list_metadata['list-item-type']) ? $list_metadata['list-item-type'] : 'string'; $this->sub_engine = drush_load_engine('outputformat', $sub_formatter, $this->engine_config); if (!is_object($this->sub_engine)) { return drush_set_error('DRUSH_INVALID_SUBFORMATTER', dt("The list output formatter could not load its subformatter: !sub", array('!sub' => $sub_formatter))); } $engine_info = $this->engine_config['engine-info']; $this->engine_config = array( 'engine-info' => array( 'machine-parsable' => $this->sub_engine->engine_config['engine-info']['machine-parsable'], ), 'metameta' => $this->sub_engine->engine_config, ) + $list_metadata; return TRUE; } function format($input, $metadata) { $output = ''; if (is_array($input)) { // If this list is processing output from a command that produces table // @todo - need different example below? // output, but our subformatter only supports 'single' items (e.g. csv), // then we will convert our data such that the output will be the keys // of the table rows. if (($this->data_type($metadata) == 'format-table') && ($this->supports_single_only($metadata)) && !isset($metadata['list-item'])) { // If the user specified exactly one field with --fields, then // use it to select the data column to use instead of the array key. if (isset($metadata['field-labels']) && (count($metadata['field-labels']) == 1)) { $first_label = key($metadata['field-labels']); $input = drush_output_get_selected_field($input, $first_label); } else { $input = array_keys($input); } } $first = TRUE; $field_selection_control = isset($metadata['list-field-selection-control']) ? $metadata['list-field-selection-control'] : 0; $selected_output_fields = false; if (empty($metadata['metameta'])) { $metameta = $metadata; unset($metameta['list-item']); unset($metameta['list-item-default-value']); } else { $metameta = $metadata['metameta']; } $list_separator_key = 'list-separator'; if ($this->sub_engine->is_list()) { $list_separator_key = 'line-separator'; if (isset($metadata['list-separator'])) { $metameta['list-separator'] = $metadata['list-separator']; } } $separator = isset($metadata[$list_separator_key]) && !empty($metadata[$list_separator_key]) ? $metadata[$list_separator_key] : "\n"; // @todo - bug? we iterate over a hard coded, single item array? foreach (array('field-labels') as $key) { if (isset($metadata[$key])) { $metameta[$key] = $metadata[$key]; } } // Include field labels, if specified if (!isset($metadata['list-item']) && isset($metadata['labeled-list']) && is_array($input) && isset($metadata['field-labels'])) { if (isset($metadata['include-field-labels']) && $metadata['include-field-labels']) { array_unshift($input, $metadata['field-labels']); } } foreach ($input as $label => $data) { // If this output formatter is set to print a single item from each // element, select that item here. if (isset($metadata['list-item'])) { $data = isset($data[$metadata['list-item']]) ? $data[$metadata['list-item']] : $metadata['list-item-default-value']; } // If this formatter supports the --fields option, then filter and // order the fields the user wants here. Note that we need to be // careful about when we call drush_select_output_fields(); sometimes, // there will be nested formatters of type 'list', and it would not // do to select the output fields more than once. // 'list-field-selection-control can be set to a positive number to // cause output fields to be selected at a later point in the call chain. elseif (is_array($data) && isset($metadata['field-labels'])) { if (!$field_selection_control) { $data = drush_select_output_fields($data, $metadata['field-labels'], $metadata['field-mappings']); $selected_output_fields = true; } } $metameta['label'] = $label; if ($selected_output_fields) { $metameta['list-field-selection-control'] = -1; } elseif ($field_selection_control) { $metameta['list-field-selection-control'] = $field_selection_control - 1; } $formatted_item = $this->sub_engine->format($data, $metameta); if ($formatted_item === FALSE) { return FALSE; } if (!$first) { $output .= $separator; } if (($separator != "\n") && !empty($separator) && (strpos($formatted_item, $separator) !== FALSE)) { $formatted_item = drush_wrap_with_quotes($formatted_item); } $output .= $formatted_item; $first = FALSE; } } return $output; } } 1, 'b' => 2); * * Given 'message-template' == 'The first is !a and the second is !b', * output with --format=message: * * The first is 1 and the second is 2 */ class drush_outputformat_message extends drush_outputformat { function format($data, $metadata) { $result = ''; if (isset($metadata['message-template'])) { foreach ($data as $key => $value) { $data_for_dt['!' . $key] = $value; } $result = dt($metadata['message-template'], $data_for_dt); } return $result; } } array("b" => 2, "c" => 3), * "d" => array("e" => 5, "f" => 6) * ); * * Output with --format=print-r: * * Array * ( * [a] => Array * ( * [b] => 2 * [c] => 3 * ) * * [d] => Array * ( * [e] => 5 * [f] => 6 * ) * ) */ class drush_outputformat_print_r extends drush_outputformat { function format($input, $metadata) { if (is_string($input)) { $output = '"' . $input . '"'; } elseif (is_array($input) || is_object($input)) { $output = print_r($input, TRUE); } else { $output = $input; } if (isset($metadata['label'])) { $output = $metadata['label'] . ': ' . $output; } return $output; } } 1) { return $this->format_error("Multiple rows provided where only one is allowed."); } if (!empty($data)) { $data = reset($data); } if (is_array($data)) { return $this->format_error("Array provided where a string is required."); } } return (string)$data; } } array("b" => 2, "c" => 3), * "d" => array("b" => 5, "c" => 6) * ); * * Output with --format=table: * * b c * 2 3 * 5 6 */ class drush_outputformat_table extends drush_outputformat { function format($input, $metadata) { $field_list = isset($metadata['field-labels']) ? $metadata['field-labels'] : array(); $widths = array(); $col = 0; foreach($field_list as $key => $label) { if (isset($metadata['column-widths'][$key])) { $widths[$col] = $metadata['column-widths'][$key]; } ++$col; } $rows = drush_rows_of_key_value_to_array_table($input, $field_list, $metadata); $field_labels = array_key_exists('include-field-labels', $metadata) && $metadata['include-field-labels']; if (!$field_labels) { array_shift($rows); } return drush_format_table($rows, $field_labels, $widths); } } The 'table' formatter will convert an associative array into a formatted, word-wrapped table. Each item in the associative array represents one row in the table. Each row is similarly composed of associative arrays, with the key of each item indicating the column, and the value indicating the contents of the cell. See below for an example source array.

    The command core-requirements is an example of a command that produces output in a tabular format.

    $ drush core-requirements

     Title                 Severity  Description
     Cron maintenance      Error     Last run 2 weeks ago
     tasks                           Cron has not run recently. For more
                                     information, see the online handbook entry for
                                     configuring cron jobs. You can run cron
                                     manually.
     Drupal                Info      7.19
    
    (Note: the output above has been shortened for clarity; the actual output of core-requirements contains additional rows not shown here.)

    It is possible to determine the available fields by consulting drush help core requirements:

     --fields=<title, severity, description>   Fields to output. All
                                               available fields are:
                                               title, severity, sid,
                                               description, value,
                                               reason, weight.
    
    It is possible to control the fields that appear in the table, and their order, by naming the desired fields in the --fields option. The space between items is optional, so `--fields=title,sid` is valid.

    Code:

    return array (
      'cron' =>
      array (
        'title' => 'Cron maintenance tasks',
        'severity' => 2,
        'value' => 'Last run 2 weeks ago',
        'description' => 'Cron has not run recently. For more information, see the online handbook entry for configuring cron jobs. You can run cron manually.',
        'severity-label' => 'Error',
      ),
      'drupal' =>
      array (
        'title' => 'Drupal',
        'value' => '7.19',
        'severity' => -1,
        'weight' => -10,
        'severity-label' => 'Info',
      ),
    )
    
    array("b" => 2, "c" => 3), * "d" => array("e" => 5, "f" => 6) * ); * * Output with --format=var_export: * * array ( * 'a' => * array ( * 'b' => 2, * 'c' => 3, * ), * 'd' => * array ( * 'e' => 5, * 'f' => 6, * ), * ) * * Output with --format=config: (list of export) * * $config['a'] = array ( * 'b' => 2, * 'c' => 3, * ); * $config['d'] = array ( * 'e' => 5, * 'f' => 6, * ); */ class drush_outputformat_var_export extends drush_outputformat { function format($input, $metadata) { if (isset($metadata['label'])) { $variable_name = isset($metadata['variable-name']) ? $metadata['variable-name'] : 'variables'; $variable_name = preg_replace("/[^a-zA-Z0-9_-]/", "", str_replace(' ', '_', $variable_name)); $label = $metadata['label']; $label_template = (isset($metadata['label-template'])) ? $metadata['label-template'] : '$!variable["!label"] = !value;'; $output = dt($label_template, array('!variable' => $variable_name, '!label' => $label, '!value' => var_export($input, TRUE))); } else { $output = drush_var_export($input); } return $output; } } array("b" => 2, "c" => 3), * "d" => array("e" => 5, "f" => 6) * ); * * Output with --format=variables: * * $a['b'] = 2; * $a['c'] = 3; * $d['e'] = 5; * $d['f'] = 6; */ class drush_outputformat_variables extends drush_outputformat { function validate() { $metadata = $this->engine_config; $this->sub_engine = drush_load_engine('outputformat', 'var_export', $metadata); if (!is_object($this->sub_engine)) { return FALSE; } return TRUE; } function format($data, $metadata) { $output = ''; if (is_array($data)) { foreach ($data as $variable_name => $section) { foreach ($section as $label => $value) { $metameta = array( 'variable-name' => $variable_name, 'label' => $label, ); $formatted_item = $this->sub_engine->process($value, $metameta); if ($formatted_item === FALSE) { return FALSE; } $output .= $formatted_item; $output .= "\n"; } } } return $output; } } setIndentation(2); // The level where you switch to inline YAML is set to PHP_INT_MAX to // ensure this does not occur. $output = $dumper->dump($input, PHP_INT_MAX, NULL, NULL, TRUE); return $output; } } 'Output formatting options selection and use.', 'topic' => 'docs-output-formats', 'topic-file' => 'docs/output-formats.md', 'combine-help' => TRUE, 'option' => 'format', 'options' => array( 'format' => array( 'description' => 'Select output format.', 'example-value' => 'json', ), 'fields' => array( 'description' => 'Fields to output.', 'example-value' => 'field1,field2', 'value' => 'required', 'list' => TRUE, ), 'list-separator' => array( 'description' => 'Specify how elements in a list should be separated. In lists of lists, this applies to the elements in the inner lists.', 'hidden' => TRUE, ), 'line-separator' => array( 'description' => 'In nested lists of lists, specify how the outer lists ("lines") should be separated.', 'hidden' => TRUE, ), 'field-labels' => array( 'description' => 'Add field labels before first line of data. Default is on; use --no-field-labels to disable.', 'default' => '1', 'key' => 'include-field-labels', ), ), // Allow output formats to declare their // "output data type" instead of their // "required engine capability" for readability. 'config-aliases' => array( 'output-data-type' => 'require-engine-capability', ), ); return $info; } /** * Implements hook_drush_engine_ENGINE_TYPE(). * * The output format types supported are represented by * the 'engine-capabilities' of the output format engine. * The different capabilities include: * * format-single: A simple string. * * format-list: An associative array where the key * is usually the row label, and the value * is a simple string. Some list formatters * render the label, and others (like * "list" and "csv") throw it away. * * format-table: An associative array, where the key * is the row id, and the value is the * column data. The column data is also * an associative array where the key * is the column id and the value is the * cell data. The cell data should usually * be a simple string; however, some * formatters can recursively format their * cell contents before rendering (e.g. if * a cell contains a list of items in an array). * * These definitions align with the declared 'output-data-type' * declared in command records. @see drush_parse_command(). * * Any output format that does not declare any engine capabilities * is expected to be able to render any php data structure that is * passed to it. */ function outputformat_drush_engine_outputformat() { $common_topic_example = array( "a" => array("b" => 2, "c" => 3), "d" => array("e" => 5, "f" => 6) ); $engines = array(); $engines['table'] = array( 'description' => 'A formatted, word-wrapped table.', 'engine-capabilities' => array('format-table'), ); $engines['key-value'] = array( 'description' => 'A formatted list of key-value pairs.', 'engine-capabilities' => array('format-single', 'format-list', 'format-table'), 'hidden' => TRUE, ); $engines['key-value-list'] = array( 'implemented-by' => 'list', 'list-item-type' => 'key-value', 'description' => 'A list of formatted lists of key-value pairs.', 'list-field-selection-control' => 1, 'engine-capabilities' => array('format-table'), 'hidden' => TRUE, ); $engines['json'] = array( 'machine-parsable' => TRUE, 'description' => 'Javascript Object Notation.', 'topic-example' => $common_topic_example, ); $engines['string'] = array( 'machine-parsable' => TRUE, 'description' => 'A simple string.', 'engine-capabilities' => array('format-single'), ); $engines['message'] = array( 'machine-parsable' => FALSE, // depends on the label.... 'hidden' => TRUE, ); $engines['print-r'] = array( 'machine-parsable' => TRUE, 'description' => 'Output via php print_r function.', 'verbose-only' => TRUE, 'topic-example' => $common_topic_example, ); $engines['var_export'] = array( 'machine-parsable' => TRUE, 'description' => 'An array in executable php format.', 'topic-example' => $common_topic_example, ); $engines['yaml'] = array( 'machine-parsable' => TRUE, 'description' => 'Yaml output format.', 'topic-example' => $common_topic_example, ); $engines['php'] = array( 'machine-parsable' => TRUE, 'description' => 'A serialized php string.', 'verbose-only' => TRUE, 'topic-example' => $common_topic_example, ); $engines['config'] = array( 'machine-parsable' => TRUE, 'implemented-by' => 'list', 'list-item-type' => 'var_export', 'description' => "A configuration file in executable php format. The variable name is \"config\", and the variable keys are taken from the output data array's keys.", 'metadata' => array( 'variable-name' => 'config', ), 'list-field-selection-control' => -1, 'engine-capabilities' => array('format-list','format-table'), 'verbose-only' => TRUE, ); $engines['list'] = array( 'machine-parsable' => TRUE, 'list-item-type' => 'string', 'description' => 'A simple list of values.', // When a table is printed as a list, only the array keys of the rows will print. 'engine-capabilities' => array('format-list', 'format-table'), 'topic-example' => array('a', 'b', 'c'), ); $engines['nested-csv'] = array( 'machine-parsable' => TRUE, 'implemented-by' => 'list', 'list-separator' => ',', 'list-item-type' => 'csv-or-string', 'hidden' => TRUE, ); $engines['csv-or-string'] = array( 'machine-parsable' => TRUE, 'hidden' => TRUE, ); $engines['csv'] = array( 'machine-parsable' => TRUE, 'implemented-by' => 'list', 'list-item-type' => 'nested-csv', 'labeled-list' => TRUE, 'description' => 'A list of values, one per row, each of which is a comma-separated list of values.', 'engine-capabilities' => array('format-table'), 'topic-example' => array(array('a', 12, 'a@one.com'),array('b', 17, 'b@two.com')), ); $engines['variables'] = array( 'machine-parsable' => TRUE, 'description' => 'A list of php variable assignments.', 'engine-capabilities' => array('format-table'), 'verbose-only' => TRUE, 'list-field-selection-control' => -1, 'topic-example' => $common_topic_example, ); $engines['labeled-export'] = array( 'machine-parsable' => TRUE, 'description' => 'A list of php exports, labeled with a name.', 'engine-capabilities' => array('format-table'), 'verbose-only' => TRUE, 'implemented-by' => 'list', 'list-item-type' => 'var_export', 'metadata' => array( 'label-template' => '!label: !value', ), 'list-field-selection-control' => -1, 'topic-example' => $common_topic_example, ); $engines['html'] = array( 'machine-parsable' => FALSE, 'description' => 'An HTML representation', 'engine-capabilities' => array('format-table'), ); return $engines; } /** * Implements hook_drush_command_alter */ function outputformat_drush_command_alter(&$command) { // In --pipe mode, change the default format to the default pipe format, or // to json, if no default pipe format is given. if (drush_get_context('DRUSH_PIPE') && (isset($command['engines']['outputformat']))) { $default_format = isset($command['engines']['outputformat']['pipe-format']) ? $command['engines']['outputformat']['pipe-format'] : 'json'; $command['engines']['outputformat']['default'] = $default_format; } } /** * Implements hook_drush_help_alter(). */ function outputformat_drush_help_alter(&$command) { if (isset($command['engines']['outputformat'])) { $outputformat = $command['engines']['outputformat']; // If the command defines specific field labels, // then modify the help for --fields to include // specific information about the available fields. if (isset($outputformat['field-labels'])) { $all_fields = array(); $all_fields_description = array(); foreach ($outputformat['field-labels'] as $field => $human_readable) { $all_fields[] = $field; if ((strtolower($field) != strtolower($human_readable)) && !array_key_exists(strtolower($human_readable), $outputformat['field-labels'])) { $all_fields_description[] = $field . dt(" (or '!other')", array('!other' => strtolower($human_readable))); } else { $all_fields_description[] = $field; } } $field_defaults = isset($outputformat['fields-default']) ? $outputformat['fields-default'] : $all_fields; $command['options']['fields']['example-value'] = implode(', ', $field_defaults); $command['options']['fields']['description'] .= ' '. dt('All available fields are: !fields.', array('!fields' => implode(', ', $all_fields_description))); if (isset($outputformat['fields-default'])) { $command['options']['full']['description'] = dt("Show the full output, with all fields included."); } } else { // If the command does not define specific field labels, // then hide the help for --fields unless the command // uses output format engines that format tables. if (isset($outputformat['require-engine-capability']) && is_array($outputformat['require-engine-capability'])) { if (!in_array('format-table', $outputformat['require-engine-capability'])) { unset($command['options']['fields']); unset($command['options']['field-labels']); } } // If the command does define output formats, but does not // define fields, then just hide the help for the --fields option. else { $command['options']['fields']['hidden'] = TRUE; $command['options']['field-labels']['hidden'] = TRUE; } } // If the command defines a default pipe format, then // add '--pipe Equivalent to --format='. if (isset($outputformat['pipe-format'])) { if (isset($command['options']['pipe'])) { $command['options']['pipe'] .= ' '; } else { $command['options']['pipe'] = ''; } if (isset($outputformat['pipe-metadata']['message-template'])) { $command['options']['pipe'] .= dt('Displays output in the form "!message"', array('!message' => $outputformat['pipe-metadata']['message-template'])); } else { $command['options']['pipe'] .= dt("Equivalent to --format=!default.", array('!default' => $outputformat['pipe-format'])); } } } } /** * Implements hook_drush_engine_topic_additional_text(). */ function outputformat_drush_engine_topic_additional_text($engine, $instance, $config) { $result = array(); // If the output format engine has a 'topic-example' in // its configuration, then format the provided array using // the output formatter, and insert the result of the // transform into the topic text. if ($engine == 'outputformat') { if (array_key_exists('topic-example', $config)) { $code = $config['topic-example']; $formatted = drush_format($code, array(), $instance); $result[] = dt("Code:\n\nreturn !code;\n\nOutput with --format=!instance:\n\n!formatted", array('!code' => var_export($code, TRUE), '!instance' => $instance, '!formatted' => $formatted)); } } return $result; } /** * Interface for output format engines. */ class drush_outputformat { function __construct($config) { $config += array( 'column-widths' => array(), 'field-mappings' => array(), 'engine-info' => array(), ); $config['engine-info'] += array( 'machine-parsable' => FALSE, 'metadata' => array(), ); $config += $config['engine-info']['metadata']; $this->engine_config = $config; } function format_error($message) { return drush_set_error('DRUSH_FORMAT_ERROR', dt("The output data could not be processed by the selected format '!type'. !message", array('!type' => $this->engine, '!message' => $message))); } function formatter_type() { return $this->engine; } function is_list() { return FALSE; } function formatter_is_simple_list() { if (!isset($this->sub_engine)) { return false; } return ($this->formatter_type() == 'list') && ($this->sub_engine->supports_single_only()); } function data_type($metadata) { if (isset($metadata['metameta']['require-engine-capability']) && is_array($metadata['metameta']['require-engine-capability'])) { return $metadata['metameta']['require-engine-capability'][0]; } if (isset($metadata['require-engine-capability']) && is_array($metadata['require-engine-capability'])) { return $metadata['require-engine-capability'][0]; } return 'unspecified'; } function supported_data_types($metadata = NULL) { if ($metadata == NULL) { $metadata = $this->engine_config; } if (isset($metadata['metameta']['engine-info']['engine-capabilities'])) { return $metadata['metameta']['engine-info']['engine-capabilities']; } if (isset($metadata['engine-info']['engine-capabilities'])) { return $metadata['engine-info']['engine-capabilities']; } return array(); } function supports_single_only($metadata = NULL) { $supported = $this->supported_data_types($metadata); return (count($supported) == 1) && ($supported[0] == 'format-single'); } function get_info($key) { if (array_key_exists($key, $this->engine_config)) { return $this->engine_config[$key]; } elseif (isset($this->sub_engine)) { return $this->sub_engine->get_info($key); } return FALSE; } /** * Perform pre-processing and then format() the $input. */ function process($input, $metadata = array()) { $metadata = array_merge_recursive($metadata, $this->engine_config); if (isset($metadata['private-fields']) && is_array($input)) { if (!drush_get_option('show-passwords', FALSE)) { if (!is_array($metadata['private-fields'])) { $metadata['private-fields'] = array($metadata['private-fields']); } foreach ($metadata['private-fields'] as $private) { drush_unset_recursive($input, $private); } } } if (isset($metadata[$this->engine . '-metadata'])) { $engine_specific_metadata = $metadata[$this->engine . '-metadata']; unset($metadata[$this->engine . '-metadata']); $metadata = array_merge($metadata, $engine_specific_metadata); } if ((drush_get_context('DRUSH_PIPE')) && (isset($metadata['pipe-metadata']))) { $pipe_specific_metadata = $metadata['pipe-metadata']; unset($metadata['pipe-metadata']); $metadata = array_merge($metadata, $pipe_specific_metadata); } $machine_parsable = $this->engine_config['engine-info']['machine-parsable']; $formatter_type = $machine_parsable ? 'parsable' : 'formatted'; if ((!$machine_parsable) && is_bool($input)) { $input = $input ? 'TRUE' : 'FALSE'; } // Run $input through any filters that are specified for this formatter. if (isset($metadata[$formatter_type . '-filter'])) { $filters = $metadata[$formatter_type . '-filter']; if (!is_array($filters)) { $filters = array($filters); } foreach ($filters as $filter) { if (function_exists($filter)) { $input = $filter($input, $metadata); } } } if (isset($metadata['field-labels'])) { foreach (drush_hide_output_fields() as $hidden_field) { unset($metadata['field-labels'][$hidden_field]); } } return $this->format($input, $metadata); } function format($input, $metadata) { return $input; } } /** * Specify that certain fields should not appear in the resulting output. */ function drush_hide_output_fields($fields_to_hide = array()) { $already_hidden = drush_get_context('DRUSH_HIDDEN_OUTPUT_FIELDS'); if (!is_array($fields_to_hide)) { $fields_to_hide = array($fields_to_hide); } $result = array_merge($already_hidden, $fields_to_hide); drush_set_context('DRUSH_HIDDEN_OUTPUT_FIELDS', $result); return $result; } 'Run a specific queue by name', 'arguments' => array( 'queue_name' => 'The name of the queue to run, as defined in either hook_queue_info or hook_cron_queue_info.', ), 'required-arguments' => TRUE, 'options' => array( 'time-limit' => 'The maximum number of seconds allowed to run the queue', ), ); $items['queue-list'] = array( 'description' => 'Returns a list of all defined queues', 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'csv', 'field-labels' => array( 'queue' => 'Queue', 'items' => 'Items', 'class' => 'Class', ), 'ini-item' => 'items', 'table-metadata' => array( 'key-value-item' => 'items', ), 'output-data-type' => 'format-table', ), ); return $items; } /** * Validation callback for drush queue-run. */ function drush_queue_run_validate($queue_name) { try { $queue = drush_queue_get_class(); $queue->getInfo($queue_name); } catch (\Drush\Queue\QueueException $exception) { return drush_set_error('DRUSH_QUEUE_RUN_VALIDATION_ERROR', $exception->getMessage()); } } /** * Return the appropriate queue class. */ function drush_queue_get_class() { return drush_get_class('Drush\Queue\Queue'); } /** * Command callback for drush queue-run. * * Queue runner that is compatible with queues declared using both * hook_queue_info() and hook_cron_queue_info(). * * @param $queue_name * Arbitrary string. The name of the queue to work with. */ function drush_queue_run($queue_name) { $queue = drush_queue_get_class(); $time_limit = (int) drush_get_option('time-limit'); $start = microtime(TRUE); $count = $queue->run($queue_name, $time_limit); $elapsed = microtime(TRUE) - $start; drush_log(dt('Processed @count items from the @name queue in @elapsed sec.', array('@count' => $count, '@name' => $queue_name, '@elapsed' => round($elapsed, 2))), drush_get_error() ? LogLevel::WARNING : LogLevel::OK); } /** * Command callback for drush queue-list. */ function drush_queue_list() { $queue = drush_queue_get_class(); return $queue->listQueues(); } = 8) { foreach ($command['examples'] as $key => $val) { $newkey = str_replace(array('anonymous user', 'authenticated user'), array('anonymous', 'authenticated'), $key); $command['examples'][$newkey] = $val; unset($command['examples'][$key]); } } } /** * Implementation of hook_drush_command(). */ function role_drush_command() { $items['role-create'] = array( 'description' => 'Create a new role.', 'examples' => array( "drush role-create 'test role'" => "Create a new role 'test role' on D6 or D7; auto-assign the rid. On D8, 'test role' is the rid, and the human-readable name is set to 'Test role'.", "drush role-create 'test role' 'Test role'" => "Create a new role with a machine name of 'test role', and a human-readable name of 'Test role'. On D6 and D7, behaves as the previous example." ), 'arguments' => array( 'machine name' => 'The symbolic machine name for the role. Required.', 'human-readable name' => 'A descriptive name for the role. Optional; Drupal 8 only. Ignored in D6 and D7.', ), 'aliases' => array('rcrt'), ); $items['role-delete'] = array( 'description' => 'Delete a role.', 'examples' => array( "drush role-delete 'test role'" => "Delete the role 'test role'.", ), 'arguments' => array( 'machine name' => 'The symbolic machine name for the role. Required. In D6 and D7, this may also be a numeric role ID.', ), 'aliases' => array('rdel'), ); $items['role-add-perm'] = array( 'description' => 'Grant specified permission(s) to a role.', 'examples' => array( "drush role-add-perm 'anonymous user' 'post comments'" => 'Allow anon users to post comments.', "drush role-add-perm 'anonymous user' \"'post comments','access content'\"" => 'Allow anon users to post comments and access content.', "drush role-add-perm 'authenticated user' --module=node" => 'Select a permission from "node" permissions to add to logged in users.' ), 'arguments' => array( 'role' => 'The role to modify. Required.', 'permissions' => 'The list of permission to grant, delimited by commas. Required, unless the --module option is used.', ), 'required-arguments' => 1, 'options' => array( 'module' => 'Select the permission to modify from an interactive list of all permissions available in the specified module.', ), 'global-options' => array( 'cache-clear', ), 'aliases' => array('rap'), ); $items['role-remove-perm'] = array( 'description' => 'Remove specified permission(s) from a role.', 'examples' => array( "drush role-remove-perm 'anonymous user' 'access content'" => 'Hide content from anon users.', ), 'arguments' => array( 'role' => 'The role to modify.', 'permissions' => 'The list of permission to grant, delimited by commas. Required, unless the --module option is used.', ), 'required-arguments' => 1, 'options' => array( 'module' => 'Select the permission to modify from an interactive list of all permissions available in the specified module.', ), 'global-options' => array( 'cache-clear', ), 'aliases' => array('rmp'), ); $items['role-list'] = array( 'description' => 'Display a list of all roles defined on the system. If a role name is provided as an argument, then all of the permissions of that role will be listed. If a permission name is provided as an option, then all of the roles that have been granted that permission will be listed.', 'examples' => array( "drush role-list --filter='administer nodes'" => 'Display a list of roles that have the administer nodes permission assigned.', "drush role-list 'anonymous user'" => 'Display all of the permissions assigned to the anon user role.' ), 'arguments' => array( 'role' => 'The role to list. Optional; if specified, lists all permissions assigned to that role. If no role is specified, lists all of the roles available on the system.', ), 'options' => array( 'filter' => 'Limits the list of roles to only those that have been assigned the specified permission. Optional; may not be specified if a role argument is provided.', ), 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'list', 'field-labels' => array('rid' => 'ID', 'label' => 'Role Label', 'perm' => "Permission"), 'output-data-type' => 'format-table', ), 'aliases' => array('rls'), ); return $items; } /** * Create the specified role */ function drush_role_create($rid, $role_name = '') { $role = drush_role_get_class(); $result = $role->role_create($rid, $role_name); if ($result !== FALSE) { drush_log(dt('Created "!role"', array('!role' => $rid)), LogLevel::SUCCESS); } return $result; } /** * Create the specified role */ function drush_role_delete($rid) { $role = drush_role_get_class($rid); if ($role === FALSE) { return FALSE; } $result = $role->delete(); if ($result !== FALSE) { drush_log(dt('Deleted "!role"', array('!role' => $rid)), LogLevel::SUCCESS); } return $result; } /** * Add one or more permission(s) to the specified role. * * @param string $rid machine name for a role * @param null|string $permissions machine names, delimited by commas. * * @return bool */ function drush_role_add_perm($rid, $permissions = NULL) { if (is_null($permissions)) { // Assume --module is used thus inject a FALSE $perms = array(FALSE); } else { $perms = _convert_csv_to_array($permissions); } $added_perm = FALSE; foreach($perms as $perm) { $result = drush_role_perm('add', $rid, $perm); if ($result !== FALSE) { $added_perm = TRUE; drush_log(dt('Added "!perm" to "!role"', array('!perm' => $perm, '!role' => $result->name)), LogLevel::SUCCESS); } } if ($added_perm) { drush_drupal_cache_clear_all(); } return $result; } /** * Remove permission(s) from the specified role. * * @param string $rid machine name for a role * @param null|string $permissions machine names, delimited by commas. * * @return bool */ function drush_role_remove_perm($rid, $permissions = NULL) { if (is_null($permissions)) { // Assume --module is used thus inject a FALSE $perms = array(FALSE); } else { $perms = _convert_csv_to_array($permissions); } $removed_perm = FALSE; foreach($perms as $perm) { $result = drush_role_perm('remove', $rid, $perm); if ($result !== FALSE) { $removed_perm = TRUE; drush_log(dt('Removed "!perm" from "!role"', array('!perm' => $perm, '!role' => $result->name)), LogLevel::OK); } } if ($removed_perm) { drush_drupal_cache_clear_all(); } return $result; } /** * Implement permission add / remove operations. * * @param string $action 'add' | 'remove' * @param string $rid * @param string|null $permission * * @return bool|RoleBase * * @see drush_set_error() */ function drush_role_perm($action, $rid, $permission = NULL) { $role = drush_role_get_class($rid); if (!$role) { return FALSE; } // If a permission wasn't provided, but the module option is specified, // provide a list of permissions provided by that module. if (!$permission && $module = drush_get_option('module', FALSE)) { drush_include_engine('drupal', 'environment'); if (!drush_module_exists($module)) { return drush_set_error('DRUSH_ROLE_ERROR', dt('!module not enabled!', array('!module' => $module))); } $module_perms = $role->getModulePerms($module); if (empty($module_perms)) { return drush_set_error('DRUSH_ROLE_NO_PERMISSIONS', dt('No permissions found for module !module', array('!module' => $module))); } $choice = drush_choice($module_perms, "Enter a number to choose which permission to $action."); if ($choice === FALSE) { return drush_user_abort(); } $permission = $module_perms[$choice]; } else { $permissions = $role->getAllModulePerms(); if (!in_array($permission, $permissions)) { return drush_set_error(dt('Could not find the permission: !perm', array('!perm' => $permission))); } } $result = $role->{$action}($permission); if ($result === FALSE) { return FALSE; } return $role; } /** * Get core version specific Role handler class. * * @param string $role_name * @return RoleBase * * @see drush_get_class(). */ function drush_role_get_class($role_name = DRUPAL_ANONYMOUS_RID) { return drush_get_class('Drush\Role\Role', func_get_args()); } /** * Displays a list of roles */ function drush_role_list($rid = '') { $result = array(); if (empty($rid)) { drush_hide_output_fields(array('perm')); // Get options passed. $perm = drush_get_option('filter'); $roles = array(); // Get all roles - if $perm is empty user_roles retrieves all roles. $roles = user_roles(FALSE, $perm); if (empty($roles)) { return drush_set_error('DRUSH_NO_ROLES', dt("No roles found.")); } foreach ($roles as $rid => $value) { $role = drush_role_get_class($rid); $result[$role->name] = array( 'rid' => $rid, 'label' => $role->name, ); } } else { drush_hide_output_fields(array('rid', 'label')); $role = drush_role_get_class($rid); if (!$role) { return FALSE; } $perms = $role->getPerms(); foreach ($perms as $permission) { $result[$permission] = array( 'perm' => $permission); } } return $result; } $source))); } if (!isset($destination_settings)) { return drush_set_error('DRUSH_BAD_PATH', dt('Could not evaluate destination path !path.', array('!path' => $destination))); } // If the user path is the same for the source and the destination, then // always add a slash to the end of the source. If the user path is not // the same in the source and the destination, then you need to know how // rsync paths work, and put on the trailing '/' if you want it. if ($source_settings['user-path'] == $destination_settings['user-path']) { $source_path .= '/'; } // Prompt for confirmation. This is destructive. if (!drush_get_context('DRUSH_SIMULATE')) { drush_print(dt("You will delete files in !target and replace with data from !source", array('!source' => $source_path, '!target' => $destination_path))); if (!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } } // Next, check to see if both the source and the destination are remote. // If so, then we'll process this as an rsync from source to local, // followed by an rsync from local to the destination. if (drush_sitealias_is_remote_site($source_settings) && drush_sitealias_is_remote_site($destination_settings)) { return _drush_core_rsync_both_remote($source, $destination, $additional_options, $source_path); } // Exclude settings is the default only when both the source and // the destination are aliases or site names. Therefore, include // settings will be the default whenever either the source or the // destination contains a : or a /. $include_settings_is_default = (strpos($source . $destination, ':') !== FALSE) || (strpos($source . $destination, '/') !== FALSE); $options = _drush_build_rsync_options($additional_options, $include_settings_is_default); // Get all of the args and options that appear after the command name. $original_args = drush_get_original_cli_args_and_options(); foreach ($original_args as $original_option) { if ($original_option{0} == '-') { $options .= ' ' . $original_option; } } drush_backend_set_result($destination_path); // Go ahead and call rsync with the paths we determined return drush_core_exec_rsync($source_path, $destination_path, $options); } /** * Make a direct call to rsync after the source and destination paths * have been evaluated. * * @param $source * Any path that can be passed to rsync. * @param $destination * Any path that can be passed to rsync. * @param $additional_options * An array of options that overrides whatever was passed in on the command * line (like the 'process' context, but only for the scope of this one * call). * @param $include_settings_is_default * If TRUE, then settings.php will be transferred as part of the rsync unless * --exclude-conf is specified. If FALSE, then settings.php will be excluded * from the transfer unless --include-conf is specified. * @param $live_output * If TRUE, output goes directly to the terminal using system(). If FALSE, * rsync is executed with drush_shell_exec() with output in * drush_shell_exec_output(). * * @return * TRUE on success, FALSE on failure. */ function drush_core_call_rsync($source, $destination, $additional_options = array(), $include_settings_is_default = TRUE, $live_output = TRUE) { $options = _drush_build_rsync_options($additional_options, $include_settings_is_default); return drush_core_exec_rsync($source, $destination, $options, $additional_options, $live_output); } function drush_core_exec_rsync($source, $destination, $options, $additional_options = array(), $live_output = TRUE) { $ssh_options = drush_get_option_override($additional_options, 'ssh-options', ''); $exec = "rsync -e 'ssh $ssh_options' $options $source $destination"; if ($live_output) { $exec_result = drush_op_system($exec); $result = ($exec_result == 0); } else { $result = drush_shell_exec($exec); } if (!$result) { drush_set_error('DRUSH_RSYNC_FAILED', dt("Could not rsync from !source to !dest", array('!source' => $source, '!dest' => $destination))); } return $result; } function _drush_build_rsync_options($additional_options, $include_settings_is_default = TRUE) { $options = ''; // Exclude vcs reserved files. if (!_drush_rsync_option_exists('include-vcs', $additional_options)) { $vcs_files = drush_version_control_reserved_files(); foreach ($vcs_files as $file) { $options .= ' --exclude="'.$file.'"'; } } else { unset($additional_options['include-vcs']); } $mode = '-akz'; // Process --include-paths and --exclude-paths options the same way foreach (array('include', 'exclude') as $include_exclude) { // Get the option --include-paths or --exclude-paths and explode to an array of paths // that we will translate into an --include or --exclude option to pass to rsync $inc_ex_path = explode(PATH_SEPARATOR, drush_get_option($include_exclude . '-paths', '')); foreach ($inc_ex_path as $one_path_to_inc_ex) { if (!empty($one_path_to_inc_ex)) { $options .= ' --' . $include_exclude . '="' . $one_path_to_inc_ex . '"'; } } // Remove stuff inserted by evaluate path unset($additional_options[$include_exclude . '-paths']); unset($additional_options[$include_exclude . '-files-processed']); } // drush_core_rsync passes in $include_settings_is_default such that // 'exclude-conf' is the default when syncing from one alias to // another, and 'include-conf' is the default when a path component // is included. if ($include_settings_is_default ? _drush_rsync_option_exists('exclude-conf', $additional_options) : !_drush_rsync_option_exists('include-conf', $additional_options)) { $options .= ' --exclude="settings.php"'; unset($additional_options['exclude-conf']); } if (_drush_rsync_option_exists('exclude-sites', $additional_options)) { $options .= ' --include="sites/all" --exclude="sites/*"'; unset($additional_options['exclude-sites']); } if (_drush_rsync_option_exists('mode', $additional_options)) { $mode = "-" . drush_get_option_override($additional_options, 'mode'); unset($additional_options['mode']); } if (drush_get_context('DRUSH_VERBOSE')) { // the drush_op() will be verbose about the command that gets executed. $mode .= 'v'; $options .= ' --stats --progress'; } // Check if the user has set $options['rsync-version'] to enable rsync legacy version support. // Drush was written for rsync 2.6.9 or later, so assume that version if nothing was explicitly set. $rsync_version = drush_get_option(array('rsync-version','source-rsync-version','target-rsync-version'), '2.6.9'); $options_to_exclude = array('ssh-options'); foreach ($additional_options as $test_option => $value) { // Downgrade some options for older versions of rsync if ($test_option == 'remove-source-files') { if (version_compare($rsync_version, '2.6.4', '<')) { $test_option = NULL; drush_log('Rsync does not support --remove-sent-files prior to version 2.6.4; some temporary files may remain undeleted.', LogLevel::WARNING); } elseif (version_compare($rsync_version, '2.6.9', '<')) { $test_option = 'remove-sent-files'; } } if ((isset($test_option)) && !in_array($test_option, $options_to_exclude) && (isset($value) && !is_array($value))) { if (($value === TRUE) || (!isset($value))) { $options .= " --$test_option"; } else { $options .= " --$test_option=" . escapeshellarg($value); } } } return $mode . $options; } function _drush_rsync_option_exists($option, $additional_options) { if (array_key_exists($option, $additional_options)) { return TRUE; } else { return drush_get_option($option, FALSE); } } /** * Handle an rsync operation from a remote site to a remote * site by first rsync'ing to a local location, and then * copying that location to its final destination. */ function _drush_core_rsync_both_remote($source, $destination, $additional_options, $source_path) { $options = $additional_options + drush_redispatch_get_options(); // Make a temporary directory to copy to. There are three // cases to consider: // // 1. rsync @src:file.txt @dest:location // 2. rsync @src:dir @dest:location // 3. rsync @src:dir/ @dest:location // // We will explain each of these in turn. // // 1. Copy a single file. We'll split this up like so: // // rsync @src:file.txt /tmp/tmpdir // rsync /tmp/tmpdir/file.txt @dest:location // // Since /tmp/tmpdir is empty, we could also rsync from // '/tmp/tmpdir/' if we wished. // // 2. Copy a directory. A directory with the same name // is copied to the destination. We'll split this up like so: // // rsync @src:dir /tmp/tmpdir // rsync /tmp/tmpdir/dir @dest:location // // The result is that everything in 'dir' is copied to @dest, // and ends up in 'location/dir'. // // 3. Copy the contents of a directory. We will split this // up as follows: // // rsync @src:dir/ /tmp/tmpdir // rsync /tmp/tmpdir/ @dest:location // // After the first rsync, everything in 'dir' will end up in // tmpdir. The second rsync copies everything in tmpdir to // @dest:location without creating an encapsulating folder // in the destination (i.e. there is no 'tmpdir' in the destination). // // All three of these cases need to be handled correctly in order // to ensure the correct results. In all cases the first // rsync always copies to $tmpDir, however the second rsync has // two cases that depend on the source path. If the source path ends // in /, the contents of a directory have been copied to $tmpDir, and // the contents of $tmpDir must be copied to the destination. Otherwise, // a specific file or directory has been copied to $tmpDir and that // specific item, identified by basename($source_path) must be copied to // the destination. $putInTmpPath = drush_tempdir(); $getFromTmpPath = "$putInTmpPath/"; if (substr($source_path, -1) !== '/') { $getFromTmpPath .= basename($source_path); } // Copy from the source to the temporary location. Exit on failure. $values = drush_invoke_process('@self', 'core-rsync', array($source, $putInTmpPath), $options); if ($values['error'] != 0) { return FALSE; } // Copy from the temporary location to the final destination. $values = drush_invoke_process('@self', 'core-rsync', array($getFromTmpPath, $destination), $options); return $values['error'] == 0; } 'Show how many items remain to be indexed out of the total.', 'drupal dependencies' => array('search'), 'outputformat' => array( 'default' => 'message', 'pipe-format' => 'message', 'field-labels' => array('remaining' => 'Items not yet indexed', 'total' => 'Total items'), 'message-template' => 'There are !remaining items out of !total still to be indexed.', 'pipe-metadata' => array( 'message-template' => '!remaining/!total', ), 'output-data-type' => 'format-list', ), ); $items['search-index'] = array( 'description' => 'Index the remaining search items without wiping the index.', 'drupal dependencies' => array('search'), ); $items['search-reindex'] = array( 'description' => 'Force the search index to be rebuilt.', 'drupal dependencies' => array('search'), 'options' => array( 'immediate' => 'Rebuild the index immediately, instead of waiting for cron.', ), ); return $items; } function drush_search_status() { list($remaining, $total) = _drush_search_status(); return array( 'remaining' => $remaining, 'total' => $total, ); } function _drush_search_status() { $remaining = 0; $total = 0; if (drush_drupal_major_version() >= 8) { $search_page_repository = \Drupal::service('search.search_page_repository'); foreach ($search_page_repository->getIndexableSearchPages() as $entity) { $status = $entity->getPlugin()->indexStatus(); $remaining += $status['remaining']; $total += $status['total']; } } elseif (drush_drupal_major_version() == 7) { foreach (variable_get('search_active_modules', array('node', 'user')) as $module) { drush_include_engine('drupal', 'environment'); $status = drush_module_invoke($module, 'search_status'); $remaining += $status['remaining']; $total += $status['total']; } } else { drush_include_engine('drupal', 'environment'); foreach (drush_module_implements('search') as $module) { // Special case. Apachesolr recommends disabling core indexing with // search_cron_limit = 0. Need to avoid infinite status loop. if ($module == 'node' && variable_get('search_cron_limit', 10) == 0) { continue; } $status = drush_module_invoke($module, 'search', 'status'); if (isset($status['remaining']) && isset($status['total'])) { $remaining += $status['remaining']; $total += $status['total']; } } } return array($remaining, $total); } function drush_search_index() { drush_op('_drush_search_index'); drush_log(dt('The search index has been built.'), LogLevel::OK); } function _drush_search_index() { list($remaining, $total) = _drush_search_status(); register_shutdown_function('search_update_totals'); $failures = 0; while ($remaining > 0) { $done = $total - $remaining; $percent = $done / $total * 100; drush_log(dt('!percent complete. Remaining items to be indexed: !count', array('!percent' => number_format($percent, 2), '!count' => $remaining)), LogLevel::OK); $eval = "register_shutdown_function('search_update_totals');"; // Use drush_invoke_process() to start subshell. Avoids out of memory issue. if (drush_drupal_major_version() >= 8) { $eval = "drush_module_invoke('search', 'cron');"; } elseif (drush_drupal_major_version() == 7) { // If needed, prod drush_module_implements() to recognize our // hook_node_update_index() implementations. drush_include_engine('drupal', 'environment'); $implementations = drush_module_implements('node_update_index'); if (!in_array('system', $implementations)) { // Note that this resets module_implements cache. drush_module_implements('node_update_index', FALSE, TRUE); } foreach (variable_get('search_active_modules', array('node', 'user')) as $module) { // TODO: Make sure that drush_module_invoke is really available when doing this eval(). $eval .= " drush_module_invoke('$module', 'update_index');"; } } else { // If needed, prod module_implements() to recognize our hook_nodeapi() // implementations. $implementations = module_implements('nodeapi'); if (!in_array('system', $implementations)) { // Note that this resets module_implements cache. module_implements('nodeapi', FALSE, TRUE); } $eval .= " module_invoke_all('update_index');"; } drush_invoke_process('@self', 'php-eval', array($eval)); $previous_remaining = $remaining; list($remaining, ) = _drush_search_status(); // Make sure we're actually making progress. if ($remaining == $previous_remaining) { $failures++; if ($failures == 3) { drush_log(dt('Indexing stalled with @number items remaining.', array( '@number' => $remaining, )), LogLevel::ERROR); return; } } // Only count consecutive failures. else { $failures = 0; } } } function drush_search_reindex() { drush_print(dt("The search index must be fully rebuilt before any new items can be indexed.")); if (drush_get_option('immediate')) { drush_print(dt("Rebuilding the index may take a long time.")); } if (!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } if (drush_drupal_major_version() == 8) { // D8 CR: https://www.drupal.org/node/2326575 $search_page_repository = \Drupal::service('search.search_page_repository'); foreach ($search_page_repository->getIndexableSearchPages() as $entity) { $entity->getPlugin()->markForReindex(); } } elseif (drush_drupal_major_version() == 7) { drush_op('search_reindex'); } else { drush_op('search_wipe'); } if (drush_get_option('immediate')) { drush_op('_drush_search_index'); drush_log(dt('The search index has been rebuilt.'), LogLevel::OK); } else { drush_log(dt('The search index will be rebuilt.'), LogLevel::OK); } } /** * Fake an implementation of hook_node_update_index() for Drupal 7. */ function system_node_update_index($node) { // Verbose output. if (drush_get_context('DRUSH_VERBOSE')) { $nid = $node->nid; if (is_object($nid)) { // In D8, this is a FieldItemList. $nid = $nid->value; } drush_log(dt('Indexing node !nid.', array('!nid' => $nid)), LogLevel::OK); } } /** * Fake an implementation of hook_nodeapi() for Drupal 6. */ function system_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { if ($op == 'update index') { // Verbose output. if (drush_get_context('DRUSH_VERBOSE')) { drush_log(dt('Indexing node !nid.', array('!nid' => $node->nid)), LogLevel::OK); } } } array_keys($all)); } } function shellalias_drush_command() { $items = array(); $items['shell-alias'] = array( 'description' => 'Print all known shell alias records.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'arguments' => array( 'alias' => 'Shell alias to print', ), 'outputformat' => array( 'default' => 'key-value', 'pipe-format' => 'json', 'simplify-single' => TRUE, 'output-data-type' => 'format-list', ), 'aliases' => array('sha'), 'examples' => array( 'drush shell-alias' => 'List all alias records known to drush.', 'drush shell-alias pull' => 'Print the value of the shell alias \'pull\'.', ), ); return $items; } /** * Print out the specified shell aliases. */ function drush_core_shell_alias($alias = FALSE) { $shell_aliases = drush_get_context('shell-aliases', array()); if (!$alias) { return $shell_aliases; } elseif (isset($shell_aliases[$alias])) { return array($alias => $shell_aliases[$alias]); } } 'Install Drupal along with modules/themes/configuration using the specified install profile.', 'arguments' => array( // In Drupal 7 installation profiles can be marked as exclusive by placing // a // @code // exclusive: true // @endcode // line in the profile's info file. // See https://www.drupal.org/node/1022020 for more information. // // In Drupal 8 you can turn your installation profile into a distribution // by providing a // @code // distribution: // name: 'Distribution name' // @endcode // block in the profile's info YAML file. // See https://www.drupal.org/node/2210443 for more information. 'profile' => 'The install profile you wish to run. Defaults to \'default\' in D6, \'standard\' in D7+, unless an install profile is marked as exclusive (or as a distribution in D8+ terminology) in which case that is used.', 'key=value...' => 'Any additional settings you wish to pass to the profile. Fully supported on D7+, partially supported on D6 (single step configure forms only). The key is in the form [form name].[parameter name] on D7 or just [parameter name] on D6.', ), 'options' => array( 'db-url' => array( 'description' => 'A Drupal 6 style database URL. Only required for initial install - not re-install.', 'example-value' => 'mysql://root:pass@host/db', ), 'db-prefix' => 'An optional table prefix to use for initial install. Can be a key-value array of tables/prefixes in a drushrc file (not the command line).', 'db-su' => array( 'description' => 'Account to use when creating a new database. Must have Grant permission (mysql only). Optional.', 'example-value' => 'root', ), 'db-su-pw' => array( 'description' => 'Password for the "db-su" account. Optional.', 'example-value' => 'pass', ), 'account-name' => 'uid1 name. Defaults to admin', 'account-pass' => 'uid1 pass. Defaults to a randomly generated password. If desired, set a fixed password in drushrc.php.', 'account-mail' => 'uid1 email. Defaults to admin@example.com', 'locale' => array( 'description' => 'A short language code. Sets the default site language. Language files must already be present. You may use download command to get them.', 'example-value' => 'en-GB', ), 'clean-url'=> 'Defaults to clean; use --no-clean-url to disable. Note that Drupal 8 and later requires clean.', 'site-name' => 'Defaults to Site-Install', 'site-mail' => 'From: for system mailings. Defaults to admin@example.com', 'sites-subdir' => array( 'description' => "Name of directory under 'sites' which should be created. Only needed when the subdirectory does not already exist. Defaults to 'default'", 'value' => 'required', 'example-value' => 'directory_name', ), 'config-dir' => 'A path pointing to a full set of configuration which should be imported after installation.', ), 'examples' => array( 'drush site-install expert --locale=uk' => '(Re)install using the expert install profile. Set default language to Ukrainian.', 'drush site-install --db-url=mysql://root:pass@localhost:port/dbname' => 'Install using the specified DB params.', 'drush site-install --db-url=sqlite://sites/example.com/files/.ht.sqlite' => 'Install using SQLite (D7+ only).', 'drush site-install --account-name=joe --account-pass=mom' => 'Re-install with specified uid1 credentials.', 'drush site-install standard install_configure_form.site_default_country=FR my_profile_form.my_settings.key=value' => 'Pass additional arguments to the profile (D7 example shown here - for D6, omit the form id).', "drush site-install standard install_configure_form.update_status_module='array(FALSE,FALSE)'" => 'Disable email notification during install and later (D7). If your server has no mail transfer agent, this gets rid of an error during install.', 'drush site-install standard install_configure_form.enable_update_status_module=NULL install_configure_form.enable_update_status_emails=NULL' => 'Disable email notification during install and later (D8). If your server has no mail transfer agent, this gets rid of an error during install.', ), 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_ROOT, 'aliases' => array('si'), ); return $items; } /** * Implements hook_drush_help_alter(). */ function site_install_drush_help_alter(&$command) { // Drupal version-specific customizations. if ($command['command'] == 'site-install') { if (drush_drupal_major_version() >= 8) { unset($command['options']['clean-url']); } else { unset($command['options']['config-dir']); } } } /** * Command validate. */ function drush_core_site_install_validate() { if ($sites_subdir = drush_get_option('sites-subdir')) { $lower = strtolower($sites_subdir); if ($sites_subdir != $lower) { drush_log(dt('Only lowercase sites-subdir are valid. Switching to !lower.', array('!lower' => $lower)), LogLevel::WARNING); drush_set_option('sites-subdir', $lower); } // Make sure that we will bootstrap to the 'sites-subdir' site. drush_set_context('DRUSH_SELECTED_URI', 'http://' . $sites_subdir); } if ($config = drush_get_option('config-dir')) { if (!file_exists($config)) { return drush_set_error('config_import_target', 'The config source directory does not exist.'); } if (!is_dir($config)) { return drush_set_error('config_import_target', 'The config source is not a directory.'); } $configFiles = glob("$config/*.yml"); if (empty($configFiles)) { drush_log(dt('Configuration import directory !config does not contain any configuration; will skip import.', array('!config' => $config)), LogLevel::WARNING); drush_set_option('config-dir', ''); } } } /** * Perform setup tasks for installation. */ function drush_core_pre_site_install($profile = NULL) { $sql = drush_sql_get_class(); if (!$db_spec = $sql->db_spec()) { drush_set_error(dt('Could not determine database connection parameters. Pass --db-url option.')); return; } // Make sure URI is set so we get back a proper $alias_record. Needed for quick-drupal. _drush_bootstrap_selected_uri(); $alias_record = drush_sitealias_get_record('@self'); $sites_subdir = drush_sitealias_local_site_path($alias_record); // Override with sites-subdir if specified. if ($dir = drush_get_option('sites-subdir')) { $sites_subdir = "sites/$dir"; } $conf_path = $sites_subdir; // Handle the case where someuse uses --variables to set the file public path. Won't work on D8+. $files = !empty($GLOBALS['conf']['files_public_path']) ? $GLOBALS['conf']['files_public_path'] : "$conf_path/files"; $settingsfile = "$conf_path/settings.php"; $sitesfile = "sites/sites.php"; $default = realpath($alias_record['root'] . '/sites/default'); $sitesfile_write = drush_drupal_major_version() >= 8 && $conf_path != $default && !file_exists($sitesfile); if (!file_exists($settingsfile)) { $msg[] = dt('create a @settingsfile file', array('@settingsfile' => $settingsfile)); } if ($sitesfile_write) { $msg[] = dt('create a @sitesfile file', array('@sitesfile' => $sitesfile)); } if ($sql->db_exists()) { $msg[] = dt("DROP all tables in your '@db' database.", array('@db' => $db_spec['database'])); } else { $msg[] = dt("CREATE the '@db' database.", array('@db' => $db_spec['database'])); } if (!drush_confirm(dt('You are about to ') . implode(dt(' and '), $msg) . ' Do you want to continue?')) { return drush_user_abort(); } // Can't install without sites subdirectory and settings.php. if (!file_exists($conf_path)) { if (!drush_mkdir($conf_path) && !drush_get_context('DRUSH_SIMULATE')) { drush_set_error(dt('Failed to create directory @conf_path', array('@conf_path' => $conf_path))); return; } } else { drush_log(dt('Sites directory @subdir already exists - proceeding.', array('@subdir' => $conf_path))); } if (!drush_file_not_empty($settingsfile)) { if (!drush_op('copy', 'sites/default/default.settings.php', $settingsfile) && !drush_get_context('DRUSH_SIMULATE')) { return drush_set_error(dt('Failed to copy sites/default/default.settings.php to @settingsfile', array('@settingsfile' => $settingsfile))); } if (drush_drupal_major_version() == 6) { // On D6, we have to write $db_url ourselves. In D7+, the installer does it. file_put_contents($settingsfile, "\n" . '$db_url = \'' . drush_get_option('db-url') . "';\n", FILE_APPEND); // Instead of parsing and performing string replacement on the configuration file, // the options are appended and override the defaults. // Database table prefix if (!empty($db_spec['db_prefix'])) { if (is_array($db_spec['db_prefix'])) { // Write db_prefix configuration as an array $db_prefix_config = '$db_prefix = ' . var_export($db_spec['db_prefix'], TRUE) . ';'; } else { // Write db_prefix configuration as a string $db_prefix_config = '$db_prefix = \'' . $db_spec['db_prefix'] . '\';'; } file_put_contents($settingsfile, "\n" . $db_prefix_config . "\n", FILE_APPEND); } } } // Write an empty sites.php if we are on D8 and using multi-site. if ($sitesfile_write) { if (!drush_op('copy', 'sites/example.sites.php', $sitesfile) && !drush_get_context('DRUSH_SIMULATE')) { return drush_set_error(dt('Failed to copy sites/example.sites.php to @sitesfile', array('@sitesfile' => $sitesfile))); } } // We need to be at least at DRUSH_BOOTSTRAP_DRUPAL_SITE to select the site uri to install to define('MAINTENANCE_MODE', 'install'); if (drush_drupal_major_version() == 6) { // The Drupal 6 installer needs to bootstrap up to the specified site. drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION); } else { drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_SITE); } if (!$sql->drop_or_create($db_spec)) { return drush_set_error(dt('Failed to create database: @error', array('@error' => implode(drush_shell_exec_output())))); } return TRUE; } /** * Command callback. */ function drush_core_site_install($profile = NULL) { $args = func_get_args(); $form_options = array(); if ($args) { // The first argument is the profile. $profile = array_shift($args); // Subsequent arguments are additional form values. foreach ($args as $arg) { list($key, $value) = explode('=', $arg, 2); // Allow for numeric and NULL values to be passed in. if (is_numeric($value)) { $value = intval($value); } elseif ($value == 'NULL') { $value = NULL; } $form_options[$key] = $value; } } // If the profile is not explicitly set, default to the 'minimal' for an issue-free config import. if (empty($profile) && drush_get_option('config-dir')) { $profile = 'minimal'; } drush_include_engine('drupal', 'site_install'); drush_core_site_install_version($profile, $form_options); // Post installation, run the configuration import. if ($config = drush_get_option('config-dir')) { // Set the destination site UUID to match the source UUID, to bypass a core fail-safe. $source_storage = new FileStorage($config); $options = ['yes' => TRUE]; drush_invoke_process('@self', 'config-set', array('system.site', 'uuid', $source_storage->read('system.site')['uuid']), $options); // Run a full configuration import. drush_invoke_process('@self', 'config-import', array(), array('source' => $config) + $options); } } 'drush_sitealias_print', 'description' => 'Print site alias records for all known site aliases and local sites.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'arguments' => array( 'site' => 'Site specification to print', ), 'options' => array( 'with-db' => 'Include the databases structure in the full alias record.', 'with-db-url' => 'Include the short-form db-url in the full alias record.', 'no-db' => 'Do not include the database record in the full alias record (default).', 'with-optional' => 'Include optional default items.', 'alias-name' => 'For a single alias, set the name to use in the output.', 'local-only' => 'Only display sites that are available on the local system (remote-site not set, and Drupal root exists).', 'show-hidden' => 'Include hidden internal elements in site alias output', ), 'outputformat' => array( 'default' => 'config', 'pipe-format' => 'var_export', 'variable-name' => 'aliases', 'hide-empty-fields' => TRUE, 'private-fields' => 'password', 'field-labels' => array('#name' => 'Name', 'root' => 'Root', 'uri' => 'URI', 'remote-host' => 'Host', 'remote-user' => 'User', 'remote-port' => 'Port', 'os' => 'OS', 'ssh-options' => 'SSH options', 'php' => 'PHP'), 'fields-default' => array('#name', 'root', 'uri', 'remote-host', 'remote-user'), 'field-mappings' => array('name' => '#name'), 'output-data-type' => 'format-table', ), 'aliases' => array('sa'), 'examples' => array( 'drush site-alias' => 'List all alias records known to drush.', 'drush site-alias @dev' => 'Print an alias record for the alias \'dev\'.', 'drush @none site-alias' => 'Print only actual aliases; omit multisites from the local Drupal installation.', ), 'topics' => array('docs-aliases'), ); $items['site-set'] = array( 'description' => 'Set a site alias to work on that will persist for the current session.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'handle-remote-commands' => TRUE, 'arguments' => array( 'site' => 'Site specification to use, or "-" for previous site. Omit this argument to "unset"', ), 'aliases' => array('use'), 'examples' => array( 'drush site-set @dev' => 'Set the current session to use the @dev alias.', 'drush site-set user@server/path/to/drupal#sitename' => 'Set the current session to use a remote site via site specification.', 'drush site-set /path/to/drupal#sitename' => 'Set the current session to use a local site via site specification.', 'drush site-set -' => 'Go back to the previously-set site (like `cd -`).', 'drush site-set' => 'Without an argument, any existing site becomes unset.', ), ); return $items; } /** * Command argument complete callback. * * @return * Array of available site aliases. */ function sitealias_site_alias_complete() { return array('values' => array_keys(_drush_sitealias_all_list())); } /** * Command argument complete callback. * * @return * Array of available site aliases. */ function sitealias_site_set_complete() { return array('values' => array_keys(_drush_sitealias_all_list())); } /** * Return a list of all site aliases known to drush. * * The array key is the site alias name, and the array value * is the site specification for the given alias. */ function _drush_sitealias_alias_list() { return drush_get_context('site-aliases'); } /** * Return a list of all of the local sites at the current drupal root. * * The array key is the site folder name, and the array value * is the site specification for that site. */ function _drush_sitealias_site_list() { $site_list = array(); $base_path = drush_get_context('DRUSH_DRUPAL_ROOT'); if ($base_path) { $base_path .= '/sites'; $files = drush_scan_directory($base_path, '/settings\.php/', array('.', '..', 'CVS', 'all'), 0, 1); foreach ($files as $filename => $info) { if ($info->basename == 'settings.php') { $alias_record = drush_sitealias_build_record_from_settings_file($filename); if (!empty($alias_record)) { $site_list[drush_sitealias_uri_to_site_dir($alias_record['uri'])] = $alias_record; } } } } return $site_list; } /** * Return the list of all site aliases and all local sites. */ function _drush_sitealias_all_list() { drush_sitealias_load_all(); return array_merge(_drush_sitealias_alias_list(), _drush_sitealias_site_list()); } /** * Return the list of site aliases (remote or local) that the * user specified on the command line. If none were specified, * then all are returned. */ function _drush_sitealias_user_specified_list() { $command = drush_get_command(); $specifications = $command['arguments']; $site_list = array(); // Iterate over the arguments and convert them to alias records if (!empty($specifications)) { list($site_list, $not_found) = drush_sitealias_resolve_sitespecs($specifications); if (!empty($not_found)) { return drush_set_error('DRUSH_ALIAS_NOT_FOUND', dt("Not found: @list", array("@list" => implode(', ', $not_found)))); } } // If the user provided no args, then we will return everything. else { drush_set_default_outputformat('list'); $site_list = _drush_sitealias_all_list(); // Filter out the hidden items foreach ($site_list as $site_name => $one_site) { if (array_key_exists('#hidden', $one_site)) { unset($site_list[$site_name]); } } } // Filter for only local sites if specified. if (drush_get_option('local-only', FALSE)) { foreach ($site_list as $site_name => $one_site) { if ( (array_key_exists('remote-site', $one_site)) || (!array_key_exists('root', $one_site)) || (!is_dir($one_site['root'])) ) { unset($site_list[$site_name]); } } } return $site_list; } /** * Print out the specified site aliases (or else all) using the format * specified. */ function drush_sitealias_print() { // Try to get the @self alias to be defined. $phase = drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_SITE); $site_list = _drush_sitealias_user_specified_list(); if ($site_list === FALSE) { return FALSE; } ksort($site_list); $with_db = (drush_get_option('with-db') != NULL) || (drush_get_option('with-db-url') != NULL); $site_specs = array(); foreach ($site_list as $site => $alias_record) { $result_record = _drush_sitealias_prepare_record($alias_record); $site_specs[$site] = $result_record; } ksort($site_specs); return $site_specs; } /** * Given a site alias name, print out a php-syntax * representation of it. * * @param alias_record * The name of the site alias to print */ function _drush_sitealias_prepare_record($alias_record) { $output_db = drush_get_option('with-db'); $output_db_url = drush_get_option('with-db-url'); $output_optional_items = drush_get_option('with-optional'); // Make sure that the default items have been added for all aliases _drush_sitealias_add_static_defaults($alias_record); // Include the optional items, if requested if ($output_optional_items) { _drush_sitealias_add_transient_defaults($alias_record); } drush_sitealias_resolve_path_references($alias_record); if (isset($output_db_url) || isset($output_db)) { drush_sitealias_add_db_settings($alias_record); } // If the user specified --with-db-url, then leave the // 'db-url' entry in the alias record (unless it is not // set, in which case we will leave the 'databases' record instead). if (isset($output_db_url)) { if (!isset($alias_record['db-url'])) { $alias_record['db-url'] = drush_sitealias_convert_databases_to_db_url($alias_record['databases']); } unset($alias_record['databases']); } // If the user specified --with-db, then leave the // 'databases' entry in the alias record. else if (isset($output_db)) { unset($alias_record['db-url']); } // If neither --with-db nor --with-db-url were specified, // then remove both the 'db-url' and the 'databases' entries. else { unset($alias_record['db-url']); unset($alias_record['databases']); } // We don't want certain fields to go into the output if (!drush_get_option('show-hidden')) { foreach ($alias_record as $key => $value) { if ($key[0] == '#') { unset($alias_record[$key]); } } } // We only want to output the 'root' item; don't output the '%root' path alias if (array_key_exists('path-aliases', $alias_record) && array_key_exists('%root', $alias_record['path-aliases'])) { unset($alias_record['path-aliases']['%root']); // If there is nothing left in path-aliases, then clear it out if (count($alias_record['path-aliases']) == 0) { unset($alias_record['path-aliases']); } } return $alias_record; } function _drush_sitealias_print_record($alias_record, $site_alias = '') { $result_record = _drush_sitealias_prepare_record($alias_record); // The alias name will be the same as the site alias name, // unless the user specified some other name on the command line. $alias_name = drush_get_option('alias-name'); if (!isset($alias_name)) { $alias_name = $site_alias; if (empty($alias_name) || is_numeric($alias_name)) { $alias_name = drush_sitealias_uri_to_site_dir($result_record['uri']); } } // Alias names contain an '@' when referenced, but do // not contain an '@' when defined. if (substr($alias_name,0,1) == '@') { $alias_name = substr($alias_name,1); } $exported_alias = var_export($result_record, TRUE); drush_print('$aliases[\'' . $alias_name . '\'] = ' . $exported_alias . ';'); } /** * Use heuristics to attempt to convert from a site directory to a URI. * This function should only be used when the URI really is unknown, as * the mapping is not perfect. * * @param site_dir * A directory, such as domain.com.8080.drupal * * @return string * A uri, such as http://domain.com:8080/drupal */ function _drush_sitealias_site_dir_to_uri($site_dir) { // Protect IP addresses NN.NN.NN.NN by converting them // temporarily to NN_NN_NN_NN for now. $uri = preg_replace("/([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)/", "$1_$2_$3_$4", $site_dir); // Convert .[0-9]+. into :[0-9]+/ $uri = preg_replace("/\.([0-9]+)\./", ":$1/", $uri); // Convert .[0-9]$ into :[0-9] $uri = preg_replace("/\.([0-9]+)$/", ":$1", $uri); // Convert .(com|net|org|info). into .(com|net|org|info)/ $uri = str_replace(array('.com.', '.net.', '.org.', '.info.'), array('.com/', '.net/', '.org/', '.info/'), $uri); // If there is a / then convert every . after the / to / // Then again, if we did this we would break if the path contained a "." // I hope that the path would never contain a "."... $pos = strpos($uri, '/'); if ($pos !== false) { $uri = substr($uri, 0, $pos + 1) . str_replace('.', '/', substr($uri, $pos + 1)); } // n.b. this heuristic works all the time if there is a port, // it also works all the time if there is a port and no path, // but it does not work for domains such as .co.jp with no path, // and it can fail horribly if someone makes a domain like "info.org". // Still, I think this is the best we can do short of consulting DNS. // Convert from NN_NN_NN_NN back to NN.NN.NN.NN $uri = preg_replace("/([0-9]+)_([0-9]+)_([0-9]+)_([0-9]+)/", "$1.$2.$3.$4", $site_dir); return 'http://' . $uri; } /** * Validation callback for drush site-set. */ function drush_sitealias_site_set_validate() { if (!function_exists('posix_getppid')) { $args = array('!command' => 'site-set', '!dependencies' => 'POSIX'); return drush_set_error('DRUSH_COMMAND_PHP_DEPENDENCY_ERROR', dt('Command !command needs the following PHP extensions installed/enabled to run: !dependencies.', $args)); } } /** * Set the DRUPAL_SITE variable by writing it out to a temporary file that we * then source for persistent site switching. * * @param site * A valid site specification. */ function drush_sitealias_site_set($site = '@none') { if ($filename = drush_sitealias_get_envar_filename()) { $last_site_filename = drush_sitealias_get_envar_filename('drush-drupal-prev-site-'); if ($site == '-') { if (file_exists($last_site_filename)) { $site = file_get_contents($last_site_filename); } else { $site = '@none'; } } if ($site == '@self') { $path = drush_cwd(); $site_record = drush_sitealias_lookup_alias_by_path($path, TRUE); if (isset($site_record['#name'])) { $site = '@' . $site_record['#name']; } else { $site = '@none'; } // Using 'site-set @self' is quiet if there is no change. $current = is_file($filename) ? trim(file_get_contents($filename)) : "@none"; if ($current == $site) { return; } } if (_drush_sitealias_set_context_by_name($site)) { if (file_exists($filename)) { @unlink($last_site_filename); @rename($filename, $last_site_filename); } $success_message = dt("Site set to !site", array('!site' => $site)); if ($site == '@none') { if (drush_delete_dir($filename)) { drush_print($success_message); } } elseif (drush_mkdir(dirname($filename), TRUE)) { if (file_put_contents($filename, $site)) { drush_print($success_message); drush_log(dt("Site information stored in !file", array('!file' => $filename))); } } } else { return drush_set_error('DRUPAL_SITE_NOT_FOUND', dt("Could not find a site definition for !site.", array('!site' => $site))); } } } 'Connect to a Drupal site\'s server via SSH for an interactive session or to run a shell command', 'arguments' => array( 'bash' => 'Bash to execute on target. Optional, except when site-alias is a list.', ), 'options' => array( 'cd' => "Directory to change to. Use a full path, TRUE for the site's Drupal root directory, or --no-cd for the ssh default (usually the remote user's home directory). Defaults to the Drupal root.", ) + drush_shell_exec_proc_build_options(), 'handle-remote-commands' => TRUE, 'strict-option-handling' => TRUE, 'examples' => array( 'drush @mysite ssh' => 'Open an interactive shell on @mysite\'s server.', 'drush @prod ssh ls /tmp' => 'Run "ls /tmp" on @prod site. If @prod is a site list, then ls will be executed on each site.', 'drush @prod ssh git pull' => 'Run "git pull" on the Drupal root directory on the @prod site.', ), 'aliases' => array('ssh'), 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'topics' => array('docs-aliases'), ); return $items; } /** * Command callback. */ function drush_ssh_site_ssh($command = NULL) { // Get all of the args and options that appear after the command name. $args = drush_get_original_cli_args_and_options(); // n.b. we do not escape the first (0th) arg to allow `drush ssh 'ls /path'` // to work in addition to the preferred form of `drush ssh ls /path`. // Supporting the legacy form means that we cannot give the full path to an // executable if it contains spaces. for ($x = 1; $x < count($args); $x++) { $args[$x] = drush_escapeshellarg($args[$x]); } $command = implode(' ', $args); if (!$alias = drush_get_context('DRUSH_TARGET_SITE_ALIAS')) { return drush_set_error('DRUSH_MISSING_TARGET_ALIAS', 'A site alias is required. The way you call ssh command has changed to `drush @alias ssh`.'); } $site = drush_sitealias_get_record($alias); // If we have multiple sites, run ourselves on each one. Set context back when done. if (isset($site['site-list'])) { if (empty($command)) { drush_set_error('DRUSH_SITE_SSH_COMMAND_REQUIRED', dt('A command is required when multiple site aliases are specified.')); return; } foreach ($site['site-list'] as $alias_single) { drush_set_context('DRUSH_TARGET_SITE_ALIAS', $alias_single); drush_ssh_site_ssh($command); } drush_set_context('DRUSH_TARGET_SITE_ALIAS', $alias); return; } if (!drush_sitealias_is_remote_site($alias)) { // Local sites run their bash without SSH. $return = drush_invoke_process('@self', 'core-execute', array($command), array('escape' => FALSE)); return $return['object']; } // We have a remote site - build ssh command and run. $interactive = FALSE; $cd = drush_get_option('cd', TRUE); if (empty($command)) { $command = 'bash -l'; $interactive = TRUE; } $cmd = drush_shell_proc_build($site, $command, $cd, $interactive); $status = drush_shell_proc_open($cmd); if ($status != 0) { return drush_set_error('DRUSH_SITE_SSH_ERROR', dt('An error @code occurred while running the command `@command`', array('@command' => $cmd, '@code' => $status))); } } 'Display a state value.', 'arguments' => array( 'key' => 'The key name.', ), 'required-arguments' => 1, 'examples' => array( 'drush state-get system.cron_last' => 'Displays last cron run timestamp', ), 'outputformat' => array( 'default' => 'json', 'pipe-format' => 'json', ), 'aliases' => array('sget'), 'core' => array('8+'), ); $items['state-set'] = array( 'description' => 'Set a state value.', 'arguments' => array( 'key' => 'The state key, for example "system.cron_last".', 'value' => 'The value to assign to the state key. Use \'-\' to read from STDIN.', ), 'required arguments' => 2, 'options' => array( 'format' => array( 'description' => 'Deprecated. See input-format option.', 'example-value' => 'boolean', 'value' => 'required', ), 'input-format' => array( 'description' => 'Type for the value. Use "auto" to detect format from value. Other recognized values are string, integer float, or boolean for corresponding primitive type, or json, yaml for complex types.', 'example-value' => 'boolean', 'value' => 'required', ), // A convenient way to pass a multiline value within a backend request. 'value' => array( 'description' => 'The value to assign to the state key (if any).', 'hidden' => TRUE, ), ), 'examples' => array( 'drush state-set system.cron_last 1406682882 --format=integer' => 'Sets a timestamp for last cron run.', 'php -r "print json_encode(array(\'drupal\', \'simpletest\'));" | drush state-set --format=json foo.name -'=> 'Set a key to a complex value (e.g. array)', ), 'aliases' => array('sset'), 'core' => array('8+'), ); $items['state-delete'] = array( 'description' => 'Delete a state value.', 'arguments' => array( 'key' => 'The state key, for example "system.cron_last".', ), 'required arguments' => 1, 'examples' => array( 'drush state-del system.cron_last' => 'Delete state entry for system.cron_last.', ), 'aliases' => array('sdel'), 'core' => array('8+'), ); return $items; } /** * State get command callback. * * @state $key * The state key. */ function drush_state_get($key = NULL) { return \Drupal::state()->get($key); } /** * State set command callback. * * @param $key * The config key. * @param $value * The data to save to state. */ function drush_state_set($key = NULL, $value = NULL) { // This hidden option is a convenient way to pass a value without passing a key. $value = drush_get_option('value', $value); if (!isset($value)) { return drush_set_error('DRUSH_STATE_ERROR', dt('No state value specified.')); } // Special flag indicating that the value has been passed via STDIN. if ($value === '-') { $value = stream_get_contents(STDIN); } // If the value is a string (usual case, unless we are called from code), // then format the input. if (is_string($value)) { $value = drush_value_format($value, drush_get_option('format', 'auto')); } \Drupal::state()->set($key, $value); } /** * State delete command callback. * * @state $key * The state key. */ function drush_state_delete($key = NULL) { \Drupal::state()->delete($key); } 'Read detailed documentation on a given topic.', 'arguments' => array( 'topic name' => 'The name of the topic you wish to view. If omitted, list all topic descriptions (and names in parenthesis).', ), 'examples' => array( 'drush topic' => 'Show all available topics.', 'drush topic docs-context' => 'Show documentation for the drush context API', 'drush docs-context' => 'Show documentation for the drush context API', ), 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'remote-tty' => TRUE, 'aliases' => array('topic'), 'topics' => array('docs-readme'), ); return $items; } /** * Implement hook_drush_help_alter(). Show 'Topics' section on help detail. */ function topic_drush_help_alter(&$command) { $implemented = drush_get_commands(); foreach ($command['topics'] as $topic_name) { // We have a related topic. Inject into the $command so the topic displays. $command['sections']['topic_section'] = 'Topics'; $command['topic_section'][$topic_name] = $implemented[$topic_name]['description']; } } /** * A command callback. * * Show a choice list of available topics and then dispatch to the respective command. * * @param string $topic_name * A command name. */ function drush_topic_core_topic($topic_name = NULL) { $commands = drush_get_commands(); $topics = drush_get_topics(); if (isset($topic_name)) { foreach (drush_get_topics() as $key => $topic) { if (strstr($key, $topic_name) === FALSE) { unset($topics[$key]); } } } if (empty($topics)) { return drush_set_error('DRUSH_NO_SUCH_TOPIC', dt("No topics on !topic found.", array('!topic' => $topic_name))); } if (count($topics) > 1) { // Show choice list. foreach ($topics as $key => $topic) { $choices[$key] = $topic['description']; } natcasesort($choices); if (!$topic_name = drush_choice($choices, dt('Choose a topic'), '!value (!key)', array(5))) { return drush_user_abort(); } } else { $keys = array_keys($topics); $topic_name = array_pop($keys); } return drush_dispatch($commands[$topic_name]); } /** * A command argument complete callback. * * @return * Available topic keys. */ function topic_core_topic_complete() { return array('values' => array_keys(drush_get_topics())); } /** * Retrieve all defined topics */ function drush_get_topics() { $commands = drush_get_commands(); foreach ($commands as $key => $command) { if (!empty($command['topic']) && empty($command['is_alias'])) { $topics[$key] = $command; } } return $topics; } DRUSH_BOOTSTRAP_NONE, 'description' => 'Show Drush usage information that has been logged but not sent. ' . $disclaimer, 'hidden' => TRUE, 'examples' => array( 'drush usage-show' => 'Show cached usage statistics.', '$options[\'drush_usage_log\'] = TRUE;' => 'Specify in a .drushrc.php file that usage information should be logged locally in a usage statistics file.', ), 'aliases' => array('ushow'), ); $items['usage-send'] = array( 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'hidden' => TRUE, 'description' => 'Send anonymous Drush usage information to statistics logging site. ' . $disclaimer, 'examples' => array( 'drush usage-send' => 'Immediately send cached usage statistics.', '$options[\'drush_usage_send\'] = TRUE;' => 'Specify in a .drushrc.php file that usage information should be sent.', '$options[\'drush_usage_size\'] = 10240;' => 'Specify the frequency (file size) that usage information should be sent.', ), 'aliases' => array('usend'), ); return $items; } /** * Log and/or send usage data to Mongolab. * * An organization can implement own hook_drush_exit() to send data to a * different endpoint. */ function usage_drush_exit() { // Ignore statistics for simulated commands. (n.b. in simulated mode, _drush_usage_mongolab will print rather than send statistics) if (!drush_get_context('DRUSH_SIMULATE')) { $file = _drush_usage_get_file(); if (drush_get_option('drush_usage_log', FALSE)) { _drush_usage_log(drush_get_command(), $file); } if (drush_get_option('drush_usage_send', FALSE)) { _drush_usage_mongolab($file, drush_get_option('drush_usage_size', 51200)); } } } /** * Set option to send usage to Mongolab. * * See usage_drush_exit() for more information. */ function drush_usage_send() { $file = _drush_usage_get_file(TRUE); if ($file) { drush_set_option('drush_usage_send', TRUE); drush_set_option('drush_usage_size', 0); drush_print(dt('To automatically send anonymous usage data, add the following to a .drushrc.php file: $options[\'drush_usage_send\'] = TRUE;')); } else { return drush_set_error('DRUSH_NO_USAGE_FILE', dt('No usage file; set $options[\'drush_usage_log\'] = TRUE; in a .drushrc.php file to enable.')); } } /** * Displays usage file. */ function drush_usage_show() { $file = _drush_usage_get_file(TRUE); if ($file) { $json = '[' . file_get_contents($file) . ']'; $usage_data = json_decode($json); foreach ($usage_data as $item) { $cmd = $item->cmd; $options = (array) $item->opt; array_unshift($options, ''); drush_print($cmd . implode(' --', $options)); } } else { return drush_set_error('DRUSH_NO_USAGE_FILE', dt('No usage file; set $options[\'drush_usage_log\'] = TRUE; in a .drushrc.php file to enable.')); } } /** * Returns path to usage file. */ function _drush_usage_get_file($required = FALSE) { $file = drush_directory_cache('usage') . '/usage.txt'; if (!file_exists($file) && $required) { return FALSE; } return $file; } function _drush_usage_log($command, $file) { $options = drush_get_command_options_extended($command); $used = drush_get_merged_options(); $command_specific = array_intersect(array_keys($used), array_keys($options)); $record = array( 'date' => $_SERVER['REQUEST_TIME'], 'cmd' => $command['command'], 'opt' => $command_specific, 'major' => DRUSH_MAJOR_VERSION, 'minor' => DRUSH_MINOR_VERSION, 'os' => php_uname('s'), 'host' => md5(php_uname('n') . get_current_user()), ); $prequel = (file_exists($file)) ? ",\n" : ""; if (file_put_contents($file, $prequel . json_encode($record), FILE_APPEND)) { drush_log(dt('Logged command and option names to local cache.'), LogLevel::DEBUG); } else { drush_log(dt('Failed to log command and option names to local cache.'), LogLevel::DEBUG); } } // We only send data periodically to save network traffic and delay. Files // are sent once they grow over 50KB (configurable). function _drush_usage_mongolab($file, $min_size_to_send) { $json = '[' . file_get_contents($file) . ']'; if (filesize($file) > $min_size_to_send) { $base = 'https://api.mongolab.com/api/1'; $apikey = '4eb95456e4b0bcd285d8135d'; // submitter account. $database = 'usage'; $collection = 'usage'; $action = "/databases/$database/collections/$collection"; $url = $base . $action . "?apiKey=$apikey"; $header = 'Content-Type: application/json'; if (!drush_shell_exec("wget -q -O - --no-check-certificate --timeout=20 --header=\"$header\" --post-data %s %s", $json, $url)) { if (!drush_shell_exec("curl -s --connect-timeout 20 --header \"$header\" --data %s %s", $json, $url)) { drush_log(dt('Drush usage statistics failed to post.'), LogLevel::DEBUG); return FALSE; } } drush_log(dt('Drush usage statistics successfully posted.'), LogLevel::DEBUG); // Empty the usage.txt file. unlink($file); return TRUE; } } 'Get a list of some or all site variables and values.', 'core' => array(6,7), 'arguments' => array( 'name' => 'A string to filter the variables by. Variables whose name contains the string will be listed.', ), 'examples' => array( 'drush vget' => 'List all variables and values.', 'drush vget user' => 'List all variables containing the string "user".', 'drush vget site_mail --exact' => 'Show only the value of the variable with the exact key "site_mail".', 'drush vget site_mail --exact --pipe' => 'Show only the variable with the exact key "site_mail" without changing the structure of the output.', ), 'options' => array( 'exact' => "Only get the one variable that exactly matches the specified name. Output will contain only the variable's value.", ), 'outputformat' => array( 'default' => 'yaml', 'pipe-format' => 'config', 'variable-name' => 'variables', 'table-metadata' => array( 'format' => 'var_export', ), ), 'aliases' => array('vget'), ); $items['variable-set'] = array( 'description' => "Set a variable.", 'core' => array(6,7), 'arguments' => array( 'name' => 'The name of a variable or the first few letters of its name.', 'value' => 'The value to assign to the variable. Use \'-\' to read the object from STDIN.', ), 'required-arguments' => TRUE, 'options' => array( 'yes' => 'Skip confirmation if only one variable name matches.', 'always-set' => array('description' => 'Older synonym for --exact; deprecated.', 'hidden' => TRUE), 'exact' => 'The exact name of the variable to set has been provided; do not prompt for similarly-named variables.', 'format' => array( 'description' => 'Type for the value. Use "auto" to detect format from value. Other recognized values are string, integer float, or boolean for corresponding primitive type, or json, yaml for complex types.', 'example-value' => 'boolean', ), ), 'examples' => array( 'drush vset --yes preprocess_css TRUE' => 'Set the preprocess_css variable to true. Skip confirmation if variable already exists.', 'drush vset --exact maintenance_mode 1' => 'Take the site offline; skips confirmation even if maintenance_mode variable does not exist. Variable is rewritten to site_offline for Drupal 6.', 'drush vset pr TRUE' => 'Choose from a list of variables beginning with "pr" to set to (bool)true.', 'php -r "print json_encode(array(\'drupal\', \'simpletest\'));" | drush vset --format=json project_dependency_excluded_dependencies -'=> 'Set a variable to a complex value (e.g. array)', ), 'aliases' => array('vset'), ); $items['variable-delete'] = array( 'core' => array(6,7), 'description' => "Delete a variable.", 'arguments' => array( 'name' => 'The name of a variable or the first few letters of its name.', ), 'required-arguments' => TRUE, 'options' => array( 'yes' => 'Skip confirmation if only one variable name matches.', 'exact' => 'Only delete the one variable that exactly matches the specified name.', ), 'examples' => array( 'drush vdel user_pictures' => 'Delete the user_pictures variable.', 'drush vdel u' => 'Choose from a list of variables beginning with "u" to delete.', 'drush vdel -y --exact maintenance_mode' => 'Bring the site back online, skipping confirmation. Variable is rewritten to site_offline for Drupal 6.', ), 'aliases' => array('vdel'), ); return $items; } /** * Command argument complete callback. */ function variable_variable_get_complete() { return variable_complete_variables(); } /** * Command argument complete callback. */ function variable_variable_set_complete() { return variable_complete_variables(); } /** * Command argument complete callback. */ function variable_variable_delete_complete() { return variable_complete_variables(); } /** * List variables for completion. * * @return * Array of available variables. */ function variable_complete_variables() { if (drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { global $conf; return array('values' => array_keys($conf)); } } /** * Command callback. * List your site's variables. */ function drush_variable_get() { global $conf; $exact = drush_get_option('exact', FALSE); $keys = array_keys($conf); if ($args = func_get_args()) { $args[0] = drush_variable_name_adjust($args[0]); if ($exact) { $keys = in_array($args[0], $keys) ? array($args[0]) : array(); } $keys = preg_grep("/{$args[0]}/", $keys); } // In --exact mode, if --pipe is not set, then simplify the return type. if ($exact && !drush_get_context('DRUSH_PIPE')) { $key = reset($keys); $returns = isset($conf[$key]) ? $conf[$key] : FALSE; } else { foreach ($keys as $name) { $value = $conf[$name]; $returns[$name] = $value; } } if (empty($keys)) { return drush_set_error('No matching variable found.'); } else { return $returns; } } /** * Command callback. * Set a variable. */ function drush_variable_set() { $args = func_get_args(); $value = $args[1]; if (!isset($value)) { return drush_set_error('DRUSH_VARIABLE_ERROR', dt('No value specified.')); } $args[0] = drush_variable_name_adjust($args[0]); $result = drush_variable_like($args[0]); $options[] = "$args[0] ". dt('(new variable)'); $match = FALSE; while (!$match && $name = drush_db_result($result)) { if ($name == $args[0]) { $options[0] = $name; $match = TRUE; } else { $options[] = $name; } } if ($value == '-') { $value = stream_get_contents(STDIN); } // If the value is a string (usual case, unless we are called from code), // then format the input if (is_string($value)) { $value = drush_value_format($value, drush_get_option('format', 'auto')); } // Format the output for display if (is_array($value)) { $display = "\n" . var_export($value, TRUE); } elseif (is_integer($value)) { $display = $value; } elseif (is_bool($value)) { $display = $value ? "TRUE" : "FALSE"; } else { $display = '"' . $value . '"'; } // Check 'always-set' for compatibility with older scripts; --exact is preferred. $always_set = drush_get_option('always-set', FALSE) || drush_get_option('exact', FALSE); if ($always_set || count($options) == 1 || $match) { variable_set($args[0], $value); drush_log(dt('!name was set to !value.', array('!name' => $args[0], '!value' => $display)), LogLevel::SUCCESS); return ''; } else { $choice = drush_choice($options, 'Enter a number to choose which variable to set.'); if ($choice === FALSE) { return drush_user_abort(); } $choice = $options[$choice]; $choice = str_replace(' ' . dt('(new variable)'), '', $choice); drush_op('variable_set', $choice, $value); drush_log(dt('!name was set to !value', array('!name' => $choice, '!value' => $display)), LogLevel::SUCCESS); } } /** * Command callback. * Delete a variable. */ function drush_variable_delete() { $args = func_get_args(); $args[0] = drush_variable_name_adjust($args[0]); // Look for similar variable names. $result = drush_variable_like($args[0]); $options = array(); while ($name = drush_db_result($result)) { $options[] = $name; } if (drush_get_option('exact', FALSE)) { $options = in_array($args[0], $options) ? array($args[0]) : array(); } if (count($options) == 0) { drush_print(dt('!name not found.', array('!name' => $args[0]))); return ''; } if ((count($options) == 1) && drush_get_context('DRUSH_AFFIRMATIVE')) { drush_op('variable_del', $args[0]); drush_log(dt('!name was deleted.', array('!name' => $args[0])), LogLevel::SUCCESS); return ''; } else { $choice = drush_choice($options, 'Enter a number to choose which variable to delete.'); if ($choice !== FALSE) { $choice = $options[$choice]; drush_op('variable_del', $choice); drush_log(dt('!choice was deleted.', array('!choice' => $choice)), LogLevel::SUCCESS); } } } // Query for similar variable names. function drush_variable_like($arg) { return drush_db_select('variable', 'name', 'name LIKE :keyword', array(':keyword' => $arg . '%'), NULL, NULL, 'name'); } // Unify similar variable names across different versions of Drupal function drush_variable_name_adjust($arg) { if (($arg == 'maintenance_mode') && (drush_drupal_major_version() < 7)) { $arg = 'site_offline'; } if (($arg == 'site_offline') && (drush_drupal_major_version() >= 7)) { $arg = 'maintenance_mode'; } return $arg; } array('8+'), 'drupal dependencies' => array('views'), ); $items['views-dev'] = array( 'description' => 'Set the Views settings to more developer-oriented values.', 'aliases' => array('vd'), ) + $base; $items['views-list'] = array( 'description' => 'Get a list of all views in the system.', 'aliases' => array('vl'), 'options' => array( 'name' => array( 'description' => 'A string contained in the view\'s name to filter the results with.', 'example-value' => 'node', 'value' => 'required', ), 'tags' => array( 'description' => 'A comma-separated list of views tags by which to filter the results.', 'example-value' => 'default', 'value' => 'required', ), 'status' => array( 'description' => 'Status of the views by which to filter the results. Choices: enabled, disabled.', 'example-value' => 'enabled', 'value' => 'required', ), ), 'examples' => array( 'drush vl' => 'Show a list of all available views.', 'drush vl --name=blog' => 'Show a list of views which names contain "blog".', 'drush vl --tags=tag1,tag2' => 'Show a list of views tagged with "tag1" or "tag2".', 'drush vl --status=enabled' => 'Show a list of enabled views.', ), 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'list', 'field-default' => array('name', 'label', 'description', 'status', 'tag'), 'field-labels' => array('name' => 'Machine Name', 'label' => 'Name', 'description' => 'Description', 'status' => 'Status', 'tag' => 'Tag'), 'output-data-type' => 'format-table', ), ) + $base; $items['views-execute'] = array( 'description' => 'Execute a view and get the results.', 'aliases' => array('vex'), 'arguments' => array( 'view' => 'The name of the view to execute.', 'display' => 'The display ID to execute. If none specified, the default display will be used.', ), 'required-arguments' => 1, 'options' => array( 'count' => array( 'description' => 'Display a count of the results instead of each row.', ), 'rendered' => array( 'description' => 'Return the results as rendered HTML output for the display.', ), 'show-admin-links' => array( 'description' => 'Show contextual admin links in the rendered markup.', ), ), 'outputformat' => array( 'default' => 'print-r', 'pipe-format' => 'var_export', ), 'examples' => array( 'drush views-execute my_view' => 'Show the result set of the default display for the my_view view.', 'drush views-execute my_view page_1 --rendered' => 'Show the rendered output of the my_view:page_1 view.', 'drush views-execute my_view page_1 3 --count' => 'Show a count of my_view:page_1 with an agument of 3 being passed.', ), ) + $base; $items['views-analyze'] = array( 'drupal dependencies' => array('views', 'views_ui'), 'description' => 'Get a list of all Views analyze warnings', 'aliases' => array('va'), 'options' => array( 'format' => array( 'description' => 'Define the output format. Known formats are: json, print_r, and export.', ), ), ) + $base; $items['views-enable'] = array( 'description' => 'Enable the specified views.', 'arguments' => array( 'views' => 'A space delimited list of view names.', ), 'required-arguments' => 1, 'aliases' => array('ven'), 'examples' => array( 'drush ven frontpage taxonomy_term' => 'Enable the frontpage and taxonomy_term views.', ), ) + $base; $items['views-disable'] = array( 'description' => 'Disable the specified views.', 'arguments' => array( 'views' => 'A space delimited list of view names.', ), 'required-arguments' => 1, 'aliases' => array('vdis'), 'examples' => array( 'drush vdis frontpage taxonomy_term' => 'Disable the frontpage and taxonomy_term views.', ), ) + $base; return $items; } /** * Command callback function for views-dev command. * * Changes the settings to more developer oriented values. */ function drush_views_dev() { $settings = array( 'ui.show.listing_filters' => TRUE, 'ui.show.master_display' => TRUE, 'ui.show.advanced_column' => TRUE, 'ui.always_live_preview' => FALSE, 'ui.always_live_preview_button' => TRUE, 'ui.show.preview_information' => TRUE, 'ui.show.sql_query.enabled' => TRUE, 'ui.show.sql_query.where' => 'above', 'ui.show.performance_statistics' => TRUE, 'ui.show.additional_queries' => TRUE, 'debug.output' => TRUE, 'debug.region' => 'message', 'ui.show.display_embed' => TRUE, ); $config = \Drupal::configFactory()->getEditable('views.settings'); foreach ($settings as $setting => $value) { $config->set($setting, $value); // Convert boolean values into a string to print. if (is_bool($value)) { $value = $value ? 'TRUE' : 'FALSE'; } // Wrap string values in quotes. elseif (is_string($value)) { $value = "\"$value\""; } drush_log(dt('!setting set to !value', array('!setting' => $setting, '!value' => $value))); } // Save the new config. $config->save(); drush_log(dt('New views configuration saved.'), LogLevel::SUCCESS); } /** * Callback function for views-list command. */ function drush_views_list() { $disabled_views = array(); $enabled_views = array(); $format = drush_get_option('format', FALSE); $views = \Drupal::entityManager()->getStorage('view')->loadMultiple(); // Get the --name option. $name = array_filter(drush_get_option_list('name')); $with_name = !empty($name) ? TRUE : FALSE; // Get the --tags option. $tags = array_filter(drush_get_option_list('tags')); $with_tags = !empty($tags) ? TRUE : FALSE; // Get the --status option. Store user input appart to reuse it after. $status = drush_get_option('status', FALSE); // Throw an error if it's an invalid status. if ($status && !in_array($status, array('enabled', 'disabled'))) { return drush_set_error(dt('Invalid status: @status. Available options are "enabled" or "disabled"', array('@status' => $status))); } // Setup a row for each view. foreach ($views as $view) { // If options were specified, check that first mismatch push the loop to the // next view. if ($with_name && !stristr($view->id(), $name[0])) { continue; } if ($with_tags && !in_array($view->get('tag'), $tags)) { continue; } $status_bool = $status == 'enabled'; if ($status && ($view->status() !== $status_bool)) { continue; } $row = array( 'name' => $view->id(), 'label' => $view->label(), 'description' => $view->get('description'), 'status' => $view->status() ? dt('Enabled') : dt('Disabled'), 'tag' => $view->get('tag'), ); // Place the row in the appropiate array, so we can have disabled views at // the bottom. if ($view->status()) { $enabled_views[] = $row; } else{ $disabled_views[] = $row; } } // Sort alphabeticaly. asort($disabled_views); asort($enabled_views); if (count($enabled_views) || count($disabled_views)) { $rows = array_merge($enabled_views, $disabled_views); return $rows; } else { drush_log(dt('No views found.')); } } /** * Drush views execute command. */ function drush_views_execute($view_name, $display_id = NULL) { $args = func_get_args(); $view_args = array(); // If it's more than 2, we have arguments. A display has to be specified in // that case. if (count($args) > 2) { $view_args = array_slice($args, 2); } if (!$view = Views::getView($view_name)) { return drush_set_error(dt('View: "@view" not found.', array('@view' => $view_name))); } // Set the display and execute the view. $view->setDisplay($display_id); $view->preExecute($view_args); $view->execute(); if (drush_get_option('count', FALSE)) { drush_set_default_outputformat('string'); return count($view->result); } elseif (!empty($view->result)) { if (drush_get_option('rendered', FALSE)) { drush_set_default_outputformat('string'); // Don't show admin links in markup by default. $view->hide_admin_links = !drush_get_option('show-admin-links', FALSE); $output = $view->preview(); return drupal_render($output); } else { return $view->result; } } else { drush_log(dt('No results returned for this view.') ,LogLevel::WARNING); return NULL; } } /** * Drush views analyze command. */ function drush_views_analyze() { $messages = NULL; $messages_count = 0; $format = drush_get_option('format', FALSE); $views = \Drupal::entityManager()->getStorage('view')->loadMultiple(); if (!empty($views)) { $analyzer = \Drupal::service('views.analyzer'); foreach ($views as $view_name => $view) { $view = $view->getExecutable(); if ($messages = $analyzer->getMessages($view)) { if ($format) { $output = drush_format($messages, $format); drush_print($output); return $output; } else { drush_print($view_name); foreach ($messages as $message) { $messages_count++; drush_print($message['type'] .': '. $message['message'], 2); } } } } drush_log(dt('A total of @total views were analyzed and @messages problems were found.', array('@total' => count($views), '@messages' => $messages_count)), LogLevel::OK); return $messages; } else { return drush_set_error(dt('There are no views to analyze')); } } /** * Drush views enable command. */ function drush_views_enable() { $view_names = func_get_args(); _views_drush_op('enable', $view_names); } /** * Drush views disable command. */ function drush_views_disable() { $view_names = func_get_args(); _views_drush_op('disable', $view_names); } /** * Perform operations on view objects. * * @param string $op * The operation to perform. * @param array $view_names * An array of view names to load and perform this operation on. */ function _views_drush_op($op = '', array $view_names = array()) { $op_types = _views_drush_op_types(); if (!in_array($op, array_keys($op_types))) { return drush_set_error(dt('Invalid op type')); } $view_names = array_combine($view_names, $view_names); if ($views = \Drupal::entityManager()->getStorage('view')->loadMultiple($view_names)) { foreach ($views as $view) { $tokens = array('@view' => $view->id(), '@action' => $op_types[$op]['action']); if ($op_types[$op]['validate']($view)) { $function = 'views_' . $op . '_view'; drush_op($function, $view); drush_log(dt('View: @view has been @action', $tokens), LogLevel::SUCCESS); } else { drush_log(dt('View: @view is already @action', $tokens), LogLevel::NOTICE); } // Remove this view from the viewnames input list. unset($view_names[$view->id()]); } return $views; } else { drush_set_error(dt('No views have been loaded')); } // If we have some unmatched/leftover view names that weren't loaded. if (!empty($view_names)) { foreach ($view_names as $viewname) { drush_log(dt('View: @view could not be found.', array('@view' => $viewname)), LogLevel::ERROR); } } } /** * Returns an array of op types that can be performed on views. * * @return array * An associative array keyed by op type => action name. */ function _views_drush_op_types() { return array( 'enable' => array( 'action' => dt('enabled'), 'validate' => '_views_drush_view_is_disabled', ), 'disable' => array( 'action' => dt('disabled'), 'validate' => '_views_drush_view_is_enabled', ), ); } /** * Returns whether a view is enabled. * * @param Drupal\views\Entity\ViewDrupal\views\ $view * The view object to check. * * @return bool * TRUE if the View is enabled, FALSE otherwise. */ function _views_drush_view_is_enabled(View $view) { return $view->status(); } /** * Returns whether a view is disabled. * * @param Drupal\views\Entity\View $view * The view object to check. * * @return bool * TRUE if the View is disabled, FALSE otherwise. */ function _views_drush_view_is_disabled(View $view) { return !$view->status(); } /** * Implements hook_cache_clear. Adds a cache clear option for views. */ function views_drush_cache_clear(&$types, $include_bootstrapped_types) { if ($include_bootstrapped_types && \Drupal::moduleHandler()->moduleExists('views')) { $types['views'] = 'views_invalidate_cache'; } } /** * Command argument complete callback. */ function views_views_enable_complete() { return _drush_views_complete(); } /** * Command argument complete callback. */ function views_views_disable_complete() { return _drush_views_complete(); } /** * Helper function to return a list of view names for complete callbacks. * * @return array * An array of available view names. */ function _drush_views_complete() { drush_bootstrap_max(); return array('values' => array_keys(\Drupal::entityManager()->getStorage('view')->loadMultiple())); } 'Show available message types and severity levels. A prompt will ask for a choice to show watchdog messages.', 'drupal dependencies' => array('dblog'), 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'var_export', 'field-labels' => array('wid' => 'ID', 'type' => 'Type', 'message' => 'Message', 'severity' => 'Severity', 'location' => 'Location', 'hostname' => 'Hostname', 'date' => 'Date', 'username' => 'Username'), 'fields-default' => array('wid', 'date', 'type', 'severity', 'message'), 'column-widths' => array('type' => 8, 'severity' => 8), 'output-data-type' => 'format-table', ), 'aliases' => array('wd-list'), ); $items['watchdog-show'] = array( 'description' => 'Show watchdog messages.', 'drupal dependencies' => array('dblog'), 'arguments' => array( 'wid' => 'Optional id of a watchdog message to show in detail. If not provided, a listing of most recent 10 messages will be displayed. Alternatively if a string is provided, watchdog messages will be filtered by it.', ), 'options' => array( 'count' => 'The number of messages to show. Defaults to 10.', 'severity' => 'Restrict to messages of a given severity level.', 'type' => 'Restrict to messages of a given type.', 'tail' => 'Continuously show new watchdog messages until interrupted.', 'sleep-delay' => 'To be used in conjunction with --tail. This is the number of seconds to wait between each poll to the database. Delay is 1 second by default.', 'extended' => 'Return extended information about each message.', ), 'examples' => array( 'drush watchdog-show' => 'Show a listing of most recent 10 messages.', 'drush watchdog-show 64' => 'Show in detail message with id 64.', 'drush watchdog-show "cron run succesful"' => 'Show a listing of most recent 10 messages containing the string "cron run succesful".', 'drush watchdog-show --count=46' => 'Show a listing of most recent 46 messages.', 'drush watchdog-show --severity=notice' => 'Show a listing of most recent 10 messages with a severity of notice.', 'drush watchdog-show --type=php' => 'Show a listing of most recent 10 messages of type php.', 'drush watchdog-show --tail --extended' => 'Show a listing of most recent 10 messages with extended information about each one and continue showing messages as they are registered in the watchdog.', 'drush watchdog-show --tail --sleep-delay=2' => 'Do a tail of the watchdog with a delay of two seconds between each poll to the database.', ), 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'var_export', 'field-labels' => array('wid' => 'ID', 'type' => 'Type', 'message' => 'Message', 'severity' => 'Severity', 'location' => 'Location', 'hostname' => 'Hostname', 'date' => 'Date', 'username' => 'Username'), 'fields-default' => array('wid', 'date', 'type', 'severity', 'message'), 'column-widths' => array('type' => 8, 'severity' => 8), 'output-data-type' => 'format-table', ), 'aliases' => array('wd-show', 'ws'), ); $items['watchdog-delete'] = array( 'description' => 'Delete watchdog messages.', 'drupal dependencies' => array('dblog'), 'options' => array( 'severity' => 'Delete messages of a given severity level.', 'type' => 'Delete messages of a given type.', ), 'examples' => array( 'drush watchdog-delete all' => 'Delete all messages.', 'drush watchdog-delete 64' => 'Delete messages with id 64.', 'drush watchdog-delete "cron run succesful"' => 'Delete messages containing the string "cron run succesful".', 'drush watchdog-delete --severity=notice' => 'Delete all messages with a severity of notice.', 'drush watchdog-delete --type=cron' => 'Delete all messages of type cron.', ), 'aliases' => array('wd-del', 'wd-delete'), ); return $items; } /** * Command callback. */ function drush_core_watchdog_list() { drush_include_engine('drupal', 'environment'); $options['-- types --'] = dt('== message types =='); $types = drush_watchdog_message_types(); foreach ($types as $key => $type) { $options[$key] = $type; } $options['-- levels --'] = dt('== severity levels =='); $severities = drush_watchdog_severity_levels(); foreach ($severities as $key => $value) { $options[$key] = "$value($key)"; } $option = drush_choice($options, dt('Select a message type or severity level.')); if ($option === FALSE) { return drush_user_abort(); } if (isset($types[$option])) { drush_set_option('type', $types[$option]); } else { drush_set_option('severity', $option - $ntypes); } return drush_core_watchdog_show_many(); } /** * Command callback. */ function drush_core_watchdog_show($arg = NULL) { drush_include_engine('drupal', 'environment'); if (is_numeric($arg)) { return drush_core_watchdog_show_one($arg); } else { return drush_core_watchdog_show_many($arg); } } /** * Print a watchdog message. * * @param $wid * The id of the message to show. */ function drush_core_watchdog_show_one($wid) { drush_set_default_outputformat('key-value-list', array('fields-default' => array('wid', 'type', 'message', 'severity', 'date'),)); $rsc = drush_db_select('watchdog', '*', 'wid = :wid', array(':wid' => $wid), 0, 1); $result = drush_db_fetch_object($rsc); if (!$result) { return drush_set_error(dt('Watchdog message #!wid not found.', array('!wid' => $wid))); } $result = core_watchdog_format_result($result, TRUE); return array($result->wid => (array)$result); } /** * Print a table of watchdog messages. * * @param $filter * String to filter the message's text by. */ function drush_core_watchdog_show_many($filter = NULL) { $count = drush_get_option('count', 10); $type = drush_get_option('type'); $severity = drush_get_option('severity'); $tail = drush_get_option('tail', FALSE); $extended = drush_get_option('extended', FALSE); $where = core_watchdog_query($type, $severity, $filter); if ($where === FALSE) { return drush_log(dt('Aborting.')); } $rsc = drush_db_select('watchdog', '*', $where['where'], $where['args'], 0, $count, 'wid', 'DESC'); if ($rsc === FALSE) { return drush_log(dt('Aborting.')); } $table = array(); while ($result = drush_db_fetch_object($rsc)) { $row = core_watchdog_format_result($result, $extended); $table[$row->wid] = (array)$row; } if (empty($table) && !$tail) { drush_log(dt('No log messages available.'), LogLevel::OK); return array(); } else { drush_log(dt('Most recent !count watchdog log messages:', array('!count' => $count))); } if ($tail) { $field_list = array('wid' => 'ID', 'date' => 'Date', 'severity' => 'Severity', 'type' => 'Type', 'message' => 'Message'); $table = array_reverse($table); $table_rows = drush_rows_of_key_value_to_array_table($table, $field_list, array()); $tbl = drush_print_table($table_rows, TRUE); // Reuse the table object to display each line generated while in tail mode. // To make it possible some hacking is done on the object: // remove the header and reset the rows on each iteration. $tbl->_headers = NULL; // Obtain the last wid. If the table has no rows, start at 0. if (count($table_rows) > 1) { $last = array_pop($table_rows); $last_wid = $last[0]; } else { $last_wid = 0; } // Adapt the where snippet. if ($where['where'] != '') { $where['where'] .= ' AND '; } $where['where'] .= 'wid > :wid'; // sleep-delay $sleep_delay = drush_get_option('sleep-delay', 1); while (TRUE) { $where['args'][':wid'] = $last_wid; $table = array(); // Reset table rows. $tbl->_data = array(); $rsc = drush_db_select('watchdog', '*', $where['where'], $where['args'], NULL, NULL, 'wid', 'ASC'); while ($result = drush_db_fetch_object($rsc)) { $row = core_watchdog_format_result($result, $extended); $table[] = array($row->wid, $row->date, $row->severity, $row->type, $row->message); #$tbl->addRow(array($row->wid, $row->date, $row->severity, $row->type, $row->message)); $last_wid = $row->wid; } $tbl->addData($table); print $tbl->_buildTable(); sleep($sleep_delay); } } return $table; } /** * Format a watchdog database row. * * @param $result * Array. A database result object. * @param $extended * Boolean. Return extended message details. * @return * Array. The result object with some attributes themed. */ function core_watchdog_format_result($result, $extended = FALSE) { // Severity. $severities = drush_watchdog_severity_levels(); $result->severity = $severities[$result->severity]; // Date. $result->date = format_date($result->timestamp, 'custom', 'd/M H:i'); unset($result->timestamp); // Message. $variables = $result->variables; if (is_string($variables)) { $variables = unserialize($variables); } if (is_array($variables)) { $result->message = strtr($result->message, $variables); } unset($result->variables); $message_length = 188; // Print all the data available if ($extended) { // Possible empty values. if (empty($result->link)) { unset($result->link); } if (empty($result->referer)) { unset($result->referer); } // Username. if ($account = user_load($result->uid)) { $result->username = $account->name; } else { $result->username = dt('Anonymous'); } unset($result->uid); $message_length = PHP_INT_MAX; } if (drush_drupal_major_version() >= 8) { $result->message = Unicode::truncate(strip_tags(Html::decodeEntities($result->message)), $message_length, FALSE, FALSE); } else { $result->message = truncate_utf8(strip_tags(decode_entities($result->message)), $message_length, FALSE, FALSE); } return $result; } /** * Command callback. * * @param $arg * The id of the message to delete or 'all'. */ function drush_core_watchdog_delete($arg = NULL) { drush_include_engine('drupal', 'environment'); if ($arg == 'all') { drush_print(dt('All watchdog messages will be deleted.')); if (!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } drush_db_delete('watchdog'); drush_log(dt('All watchdog messages have been deleted.'), LogLevel::OK); } else if (is_numeric($arg)) { drush_print(dt('Watchdog message #!wid will be deleted.', array('!wid' => $arg))); if(!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } $affected_rows = drush_db_delete('watchdog', 'wid=:wid', array(':wid' => $arg)); if ($affected_rows == 1) { drush_log(dt('Watchdog message #!wid has been deleted.', array('!wid' => $arg)), LogLevel::OK); } else { return drush_set_error(dt('Watchdog message #!wid does not exist.', array('!wid' => $arg))); } } else { $type = drush_get_option('type'); $severity = drush_get_option('severity'); if ((!isset($arg))&&(!isset($type))&&(!isset($severity))) { return drush_set_error(dt('No options provided.')); } $where = core_watchdog_query($type, $severity, $arg, 'OR'); if ($where === FALSE) { // Drush set error was already called by core_watchdog_query return FALSE; } drush_print(dt('All messages with !where will be deleted.', array('!where' => preg_replace("/message LIKE %$arg%/", "message body containing '$arg'" , strtr($where['where'], $where['args']))))); if(!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } $affected_rows = drush_db_delete('watchdog', $where['where'], $where['args']); drush_log(dt('!affected_rows watchdog messages have been deleted.', array('!affected_rows' => $affected_rows)), LogLevel::OK); } } /** * Build a WHERE snippet based on given parameters. * * @param $type * String. Valid watchdog type. * @param $severity * Int or String for a valid watchdog severity message. * @param $filter * String. Value to filter watchdog messages by. * @param $criteria * ('AND', 'OR'). Criteria for the WHERE snippet. * @return * False or array with structure ('where' => string, 'args' => array()) */ function core_watchdog_query($type = NULL, $severity = NULL, $filter = NULL, $criteria = 'AND') { $args = array(); $conditions = array(); if ($type) { $types = drush_watchdog_message_types(); if (array_search($type, $types) === FALSE) { $msg = "Unrecognized message type: !type.\nRecognized types are: !types."; return drush_set_error('WATCHDOG_UNRECOGNIZED_TYPE', dt($msg, array('!type' => $type, '!types' => implode(', ', $types)))); } $conditions[] = "type = :type"; $args[':type'] = $type; } if (isset($severity)) { $severities = drush_watchdog_severity_levels(); if (isset($severities[$severity])) { $level = $severity; } elseif (($key = array_search($severity, $severities)) !== FALSE) { $level = $key; } else { $level = FALSE; } if ($level === FALSE) { foreach ($severities as $key => $value) { $levels[] = "$value($key)"; } $msg = "Unknown severity level: !severity.\nValid severity levels are: !levels."; return drush_set_error(dt($msg, array('!severity' => $severity, '!levels' => implode(', ', $levels)))); } $conditions[] = 'severity = :severity'; $args[':severity'] = $level; } if ($filter) { $conditions[] = "message LIKE :filter"; $args[':filter'] = '%'.$filter.'%'; } $where = implode(" $criteria ", $conditions); return array('where' => $where, 'args' => $args); } $project) { $type = (isset($project['type']) && ($project['type'] == 'library')) ? 'libraries' : 'projects'; if ($previous_type != $project['_type']) { $previous_type = $project['_type']; $output[] = '; ' . ucfirst($previous_type) . 's'; } unset($project['_type']); if (!$project && is_string($name)) { $output[] = $type . '[] = "' . $name . '"'; continue; } $base = $type . '[' . $name . ']'; if (isset($project['custom_download'])) { $custom = TRUE; $output[] = '; Please fill the following out. Type may be one of get, git, bzr or svn,'; $output[] = '; and url is the url of the download.'; $output[$base . '[download][type]'] = '""'; $output[$base . '[download][url]'] = '""'; unset($project['custom_download']); } $output = array_merge($output, _drush_make_generate_lines($base, $project)); $output[] = ''; } } $string = ''; foreach ($output as $k => $v) { if (!is_numeric($k)) { $string .= $k . ' = ' . $v; } else { $string .= $v; } $string .= "\n"; } if ($custom) { drush_log(dt('Some of the properties in your makefile will have to be manually edited. Please do that now.'), LogLevel::WARNING); } return $string; } /** * Write a makefile based on data parsed from a previous makefile. * * @param $file * The path to the file to write our generated makefile to, or TRUE to * print to the terminal. * @param $makefile * A makefile on which to base our generated one. */ function make_generate_from_makefile($file, $makefile) { if (!$info = make_parse_info_file($makefile)) { return drush_set_error('MAKE_GENERATE_FAILED_PARSE', dt('Failed to parse makefile :makefile.', array(':makefile' => $makefile))); } $projects = drush_get_option('DRUSH_MAKE_PROJECTS', FALSE); if ($projects === FALSE) { $projects = make_prepare_projects(FALSE, $info); if (isset($projects['contrib'])) { $projects = array_merge($projects['core'], $projects['contrib']); } } $defaults = isset($info['defaults']) ? $info['defaults'] : array(); $core = current($projects); $core = $core['core']; foreach ($projects as $name => $project) { // If a specific revision was requested, do not set the version. if (!isset($project['revision'])) { $projects[$name]['version'] = isset($project['download']['full_version']) ? $project['download']['full_version'] : ''; if ($project['type'] != 'core' && strpos($projects[$name]['version'], $project['core']) === 0) { $projects[$name]['version'] = substr($projects[$name]['version'], strlen($project['core'] . '-')); } } else { unset($projects[$name]['version']); } $projects[$name]['_type'] = $project['type']; if ($project['download']['type'] == 'git') { drush_make_resolve_git_refs($projects[$name]); } // Don't clutter the makefile with defaults if (is_array($defaults)) { foreach ($defaults as $type => $defs) { if ($type == 'projects') { foreach ($defs as $key => $value) { if (isset($project[$key]) && $project[$key] == $value) { unset($projects[$name][$key]); } } } } } if ($project['name'] == $name) { unset($projects[$name]['name']); } if ($project['type'] == 'module' && !isset($info[$name]['type'])) { unset($projects[$name]['type']); // Module is the default } if (!(isset($project['download']['type'])) || ($project['download']['type'] == 'pm')) { unset($projects[$name]['download']); // PM is the default } $ignore = array('build_path', 'contrib_destination', 'core', 'make_directory', 'l10n_url', 'download_type'); foreach ($ignore as $key) { unset($projects[$name][$key]); } // Remove the location if it's the default. if ($projects[$name]['location'] == 'https://updates.drupal.org/release-history') { unset($projects[$name]['location']); } // Remove empty entries (e.g. 'directory_name') $projects[$name] = _make_generate_array_filter($projects[$name]); } $libraries = drush_get_option('DRUSH_MAKE_LIBRARIES', FALSE); if ($libraries === FALSE) { $libraries = isset($info['libraries']) ? $info['libraries'] : array(); } if (is_array($libraries)) { foreach ($libraries as $name => $library) { $libraries[$name]['type'] = 'library'; $libraries[$name]['_type'] = 'librarie'; if ($library['download']['type'] == 'git') { drush_make_resolve_git_refs($libraries[$name]); } } } $contents = make_generate_makefile_contents($projects, $libraries, $core, $defaults); // Write or print our makefile. $file = $file !== TRUE ? $file : NULL; make_generate_print($contents, $file); } /** * Resolve branches and revisions for git-based projects. */ function drush_make_resolve_git_refs(&$project) { if (!isset($project['download']['branch'])) { $project['download']['branch'] = drush_make_resolve_git_branch($project); } if (!isset($project['download']['revision'])) { $project['download']['revision'] = drush_make_resolve_git_revision($project); } } /** * Resolve branch for a git-based project. */ function drush_make_resolve_git_branch($project) { drush_log(dt('Resolving default branch for repo at: :repo', array(':repo' => $project['download']['url']))); if (drush_shell_exec("git ls-remote %s HEAD", $project['download']['url'])) { $head_output = drush_shell_exec_output(); list($head_commit) = explode("\t", $head_output[0]); drush_log(dt('Scanning branches in repo at: :repo', array(':repo' => $project['download']['url']))); drush_shell_exec("git ls-remote --heads %s", $project['download']['url']); $heads_output = drush_shell_exec_output(); $branches = array(); foreach ($heads_output as $key => $head) { list($commit, $ref) = explode("\t", $head); $branches[$commit] = explode("/", $ref)[2]; } $branch = $branches[$head_commit]; drush_log(dt('Resolved git branch to: :branch', array(':branch' => $branch))); return $branch; } else { drush_log(dt('Could not resolve branch for `:project` using git repo at :repo', array(':project' => $project['name'], ':repo' => $project['download']['url'])), 'warning'); } } /** * Resolve revision for a git-based project. */ function drush_make_resolve_git_revision($project) { drush_log(dt('Resolving head commit on `:branch` branch for repo at: :repo', array(':branch' => $project['download']['branch'], ':repo' => $project['download']['url']))); if (drush_shell_exec("git ls-remote %s %s", $project['download']['url'], $project['download']['branch'])) { $head_output = drush_shell_exec_output(); list($revision) = explode("\t", $head_output[0]); drush_log(dt('Resolved git revision to: :revision', array(':revision' => $revision))); return $revision; } else { drush_log(dt('Could not resolve head commit for `:project` using git repo at :repo', array(':project' => $project['name'], ':repo' => $project['download']['url'])), 'warning'); } } /** * Generate makefile contents in the appropriate format. */ function make_generate_makefile_contents($projects, $libraries = array(), $core = NULL, $defaults = array()) { $format = drush_get_option('format', 'yaml'); $func = "make_generate_makefile_contents_$format"; if (function_exists($func)) { $contents = call_user_func($func, $projects, $libraries, $core, $defaults); } else { return drush_set_error('MAKE_UNKNOWN_OUTPUT_FORMAT', dt('Generating makefiles in the :format output format is not yet supported. Implement :func() to add such support.', array(':format' => $format, ':func' => $func))); } return $contents; } /** * Generate makefile contents in (legacy) INI format. */ function make_generate_makefile_contents_ini($projects, $libraries, $core, $defaults) { return _drush_make_generate_makefile_contents($projects, $libraries, $core, $defaults); } /** * Generate makefile contents in YAML format. */ function make_generate_makefile_contents_yaml($projects, $libraries, $core, $defaults) { $info = array( 'core' => $core, 'api' => MAKE_API, 'defaults' => $defaults, 'projects' => $projects, 'libraries' => $libraries, ); $info = _make_generate_array_filter($info); $info = _make_generate_array_filter_key('_type', $info); $dumper = drush_load_engine('outputformat', 'yaml'); $yaml = $dumper->format($info, array()); return $yaml; } /** * Helper function to recursively remove empty values from an array (but not * '0'!). */ function _make_generate_array_filter($haystack) { foreach ($haystack as $key => $value) { if (is_array($value)) { $haystack[$key] = _make_generate_array_filter($haystack[$key]); } if (empty($value) && $value !== '0') { unset($haystack[$key]); } } return $haystack; } /** * Helper function to recursively remove elements matching a specific key from an array. */ function _make_generate_array_filter_key($needle, $haystack) { foreach ($haystack as $key => $value) { if ($key === $needle) { unset($haystack[$key]); } elseif (is_array($value)) { $haystack[$key] = _make_generate_array_filter_key($needle, $haystack[$key]); } } return $haystack; } /** * Print the generated makefile to the terminal, or write it to a file. * * @param $contents * The formatted contents of a makefile. * @param $file * (optional) The path to write the makefile. */ function make_generate_print($contents, $file = NULL) { if (!$file) { drush_print($contents); } elseif (file_put_contents($file, $contents)) { drush_log(dt("Wrote .make file @file", array('@file' => $file)), LogLevel::OK); } else { make_error('FILE_ERROR', dt("Unable to write .make file !file", array('!file' => $file))); } } /** * Utility function to generate the line or lines for a key/value pair in the * make file. * * @param $base * The base for the configuration lines. Values will be appended to it as * [$key] = $value, or if value is an array itself it will expand into as many * lines as required. * @param $values * May be a single value or an array. * @return * An array of strings that represent lines for the make file. */ function _drush_make_generate_lines($base, $values) { $output = array(); if (is_array($values)) { foreach ($values as $key => $value) { $newbase = $base . '[' . $key . ']'; $output = array_merge($output, _drush_make_generate_lines($newbase, $value)); } } else { $output[$base] = '"' . $values . '"'; } return $output; } function _drush_make_generate_defaults($defaults, &$output = array()) { $output[] = '; Defaults'; foreach ($defaults as $name => $project) { $type = 'defaults'; if (!$project && is_string($name)) { $output[] = $type . '[] = "' . $name . '"'; continue; } $base = $type . '[' . $name . ']'; $output = array_merge($output, _drush_make_generate_lines($base, $project)); } } 'core'); if ($core_project != 'drupal') { $projects[$core_project]['custom_download'] = TRUE; $projects[$core_project]['type'] = 'core'; } else { // Drupal core - we can determine the version if required. if (_drush_generate_track_version("drupal", $version_options)) { $projects[$core_project]["version"] = drush_drupal_version(); } } $install_profile = drush_drupal_major_version() >= 7 ? drupal_get_profile() : variable_get('install_profile', ''); if (!in_array($install_profile, array('default', 'standard', 'minimal', 'testing')) && $install_profile != '') { $projects[$install_profile]['type'] = $projects[$install_profile]['_type'] = 'profile'; $request = array( 'name' => $install_profile, 'drupal_version' => $drupal_major_version, ); if (!$release_info->checkProject($request, 'profile')) { $projects[$install_profile]['custom_download'] = TRUE; } } // Iterate installed projects to build $projects array. $extensions = $all_extensions; $project_info = drush_get_projects($extensions); foreach ($project_info as $name => $project) { // Discard the extensions within this project. At the end $extensions will // contain only extensions part of custom projects (not from drupal.org or // other update service). foreach ($project['extensions'] as $ext) { unset($extensions[$ext]); } if ($name == 'drupal') { continue; } $type = $project['type']; // Discard projects with all modules disabled. if (($type == 'module') && (!$project['status'])) { continue; } $projects[$name] = array('_type' => $type); // Check the project is on drupal.org or its own update service. $request = array( 'name' => $name, 'drupal_version' => $drupal_major_version, ); if (isset($project['status url'])) { $request['status url'] = $project['status url']; $projects[$name]['location'] = $project['status url']; } if (!$release_info->checkProject($request, $type)) { // It is not a project on drupal.org neither an external update service. $projects[$name]['type'] = $type; $projects[$name]['custom_download'] = TRUE; } // Add 'subdir' if the project is installed in a non-default location. if (isset($project['path'])) { $projects[$name] += _drush_generate_makefile_check_path($project); } // Add version number if this project's version is to be tracked. if (_drush_generate_track_version($name, $version_options) && $project["version"]) { $version = preg_replace("/^" . drush_get_drupal_core_compatibility() . "-/", "", $project["version"]); // Strip out MINOR+GIT_COMMIT strings for dev releases. if (substr($version, -4) == '-dev' && strpos($version, '+')) { $version = substr($version, 0, strrpos($version, '.')) . '.x-dev'; } $projects[$name]['version'] = $version; } foreach ($project['extensions'] as $extension_name) { _drush_make_generate_add_patch_files($projects[$name], _drush_extension_get_path($all_extensions[$extension_name])); } } // Add a project for each unknown extension. foreach ($extensions as $name => $extension) { list($project_name, $project_data) = _drush_generate_custom_project($name, $extension, $version_options); $projects[$project_name] = $project_data; } // Add libraries. if (function_exists('libraries_get_libraries')) { $libraries = libraries_get_libraries(); foreach ($libraries as $library_name => $library_path) { $path = explode('/', $library_path); $project_libraries[$library_name] = array( 'directory_name' => $path[(count($path) - 1)], 'custom_download' => TRUE, 'type' => 'library', '_type' => 'librarie', // For plural. ); } } return array($projects, $project_libraries); } /** * Record any patches that were applied to this project * per information stored in PATCHES.txt. */ function _drush_make_generate_add_patch_files(&$project, $location) { $patchfile = DRUPAL_ROOT . '/' . $location . '/PATCHES.txt'; if (is_file($patchfile)) { foreach (file($patchfile) as $line) { if (substr($line, 0, 2) == '- ') { $project['patch'][] = trim(substr($line, 2)); } } } } /** * Create a project record for an extension not downloaded from drupal.org */ function _drush_generate_custom_project($name, $extension, $version_options) { $project['_type'] = drush_extension_get_type($extension); $project['type'] = drush_extension_get_type($extension); $location = drush_extension_get_path($extension); // To start off, we will presume that our custom extension is // stored in a folder named after its project, and there are // no subfolders between the .info file and the project root. $project_name = basename($location); drush_shell_cd_and_exec($location, 'git rev-parse --git-dir 2> ' . drush_bit_bucket()); $output = drush_shell_exec_output(); if (!empty($output)) { $git_dir = $output[0]; // Find the actual base of the git repository. $repo_root = $git_dir == ".git" ? $location : dirname($git_dir); // If the repository root is at the drupal root or some parent // of the drupal root, or some other location that could not // pausibly be a project, then there is nothing we can do. // (We can't tell Drush make to download some sub-part of a repo, // can we?) if ($repo_project_name = _drush_generate_validate_repo_location($repo_root)) { $project_name = $repo_project_name; drush_shell_cd_and_exec($repo_root, 'git remote show origin'); $output = drush_shell_exec_output(); foreach ($output as $line) { if (strpos($line, "Fetch URL:") !== FALSE) { $url = preg_replace('/ *Fetch URL: */', '', $line); if (!empty($url)) { // We use the unconventional-looking keys // `download][type` and `download][url` so that // we can produce output that appears to be two-dimensional // arrays from a single-dimensional array. $project['download][type'] = 'git'; $project['download][url'] = $url; // Fill in the branch as well. drush_shell_cd_and_exec($repo_root, 'git branch'); $output = drush_shell_exec_output(); foreach ($output as $line) { if ($line{0} == '*') { $branch = substr($line, 2); if ($branch != "master") { $project['download][branch'] = $branch; } } } // Put in the commit hash. drush_shell_cd_and_exec($repo_root, 'git log'); $output = drush_shell_exec_output(); if (substr($output[0], 0, 7) == "commit ") { $revision = substr($output[0], 7); if (_drush_generate_track_version($project_name, $version_options)) { $project['download][revision'] = $revision; } } // Add patch files, if any. _drush_make_generate_add_patch_files($project, $repo_root); } } } } } // If we could not figure out where the extension came from, then give up and // flag it as a "custom" download. if (!isset($project['download][type'])) { $project['custom_download'] = TRUE; } return array($project_name, $project); } /** * If the user has checked in the Drupal root, or the 'sites/all/modules' * folder into a git repository, then we do not want to confuse that location * with a "project". */ function _drush_generate_validate_repo_location($repo_root) { $project_name = basename($repo_root); // The Drupal root, or any folder immediately inside the Drupal // root cannot be a project location. if ((strlen(DRUPAL_ROOT) >= strlen($repo_root)) || (dirname($repo_root) == DRUPAL_ROOT)) { return NULL; } // Also exclude sites/* and sites/*/{modules,themes} and profile/* and // profile/*/{modules,themes}. return $project_name; } /** * Helper function to determine if a given project is to have its version * tracked. */ function _drush_generate_track_version($project, $version_options) { // A. If --exclude-versions has been specified: // A.a. if it's a boolean, check the --include-versions option. if ($version_options["exclude"] === TRUE) { // A.a.1 if --include-versions has been specified, ensure it's an array. if (is_array($version_options["include"])) { return in_array($project, $version_options["include"]); } // A.a.2 If no include array, then we're excluding versions for ALL // projects. return FALSE; } // A.b. if --exclude-versions is an array with items, check this project is in // it: if so, then return FALSE. elseif (is_array($version_options["exclude"]) && count($version_options["exclude"])) { return !in_array($project, $version_options["exclude"]); } // B. If by now no --exclude-versions, but --include-versions is an array, // examine it for this project. if (is_array($version_options["include"]) && count($version_options["include"])) { return in_array($project, $version_options["include"]); } // If none of the above conditions match, include version number by default. return TRUE; } /** * Helper function to check for a non-default installation location. */ function _drush_generate_makefile_check_path($project) { $info = array(); $type = $project['type']; $path = dirname($project['path']); // Check to see if the path is in a subdir sites/all/modules or // profiles/profilename/modules if (preg_match('@^sites/[a-zA-Z0-9_]*/' . $type . 's/..*@', $path) || preg_match('@^sites/[a-zA-Z0-9_]*/' . $type . 's/..*@', $path)) { $subdir = preg_replace(array('@^[a-zA-Z0-9_]*/[a-zA-Z0-9_]*/' . $type . 's/*@', "@/$name" . '$@'), '', $path); if (!empty($subdir)) { $info['subdir'] = $subdir; } } return $info; } dirname($download_location), 'yes' => TRUE, 'package-handler' => 'wget', 'source' => $download['status url'], // This is only relevant for profiles, but we generally want the variant to // be 'profile-only' so we don't end up with extra copies of core. 'variant' => $type == 'core' ? 'full' : $download['variant'], 'cache' => TRUE, ); if ($type == 'core') { $options['drupal-project-rename'] = basename($download_location); } if (drush_get_option('no-cache', FALSE)) { unset($options['cache']); } $backend_options = array(); if (!drush_get_option(array('verbose', 'debug'), FALSE)) { $backend_options['integrate'] = TRUE; $backend_options['log'] = FALSE; } // Perform actual download with `drush pm-download`. $return = drush_invoke_process('@none', 'pm-download', array($full_project_version), $options, $backend_options); if (empty($return['error_log'])) { // @todo Report the URL we used for download. See // http://drupal.org/node/1452672. drush_log(dt('@project downloaded.', array('@project' => $full_project_version)), LogLevel::OK); } } /** * Downloads a file to the specified location. * * @return mixed * The destination directory on success, FALSE on failure. */ function make_download_file($name, $type, $download, $download_location, $cache_duration = DRUSH_CACHE_LIFETIME_DEFAULT) { if ($filename = _make_download_file($download['url'], $cache_duration)) { if (!drush_get_option('ignore-checksums') && !_make_verify_checksums($download, $filename)) { return FALSE; } drush_log(dt('@project downloaded from @url.', array('@project' => $name, '@url' => $download['url'])), LogLevel::OK); $download_filename = isset($download['filename']) ? $download['filename'] : ''; $subtree = isset($download['subtree']) ? $download['subtree'] : NULL; return make_download_file_unpack($filename, $download_location, $download_filename, $subtree); } make_error('DOWNLOAD_ERROR', dt('Unable to download @project from @url.', array('@project' => $name, '@url' => $download['url']))); return FALSE; } /** * Wrapper to drush_download_file(). * * @param string $download * The url of the file to download. * @param int $cache_duration * The time in seconds to cache the resultant download. * * @return string * The location of the downloaded file, or FALSE on failure. */ function _make_download_file($download, $cache_duration = DRUSH_CACHE_LIFETIME_DEFAULT) { if (drush_get_option('no-cache', FALSE)) { $cache_duration = 0; } $tmp_path = make_tmp(); // Ensure that we aren't including the querystring when generating a filename // to save our download to. $file = basename(current(explode('?', $download, 2))); return drush_download_file($download, $tmp_path . '/' . $file, $cache_duration); } /** * Unpacks a file to the specified download location. * * @return mixed * The download location on success, FALSE on failure. */ function make_download_file_unpack($filename, $download_location, $name, $subtree = NULL) { $success = FALSE; if (drush_file_is_tarball($filename)) { $tmp_location = drush_tempdir(); if (!drush_tarball_extract($filename, $tmp_location)) { return FALSE; } if ($subtree) { $tmp_location .= '/' . $subtree; if (!file_exists($tmp_location)) { return drush_set_error('DRUSH_MAKE_SUBTREE_NOT_FOUND', dt('Directory !subtree not found within !file', array('!subtree' => $subtree, '!file' => $filename))); } } else { $files = scandir($tmp_location); unset($files[0]); // . directory unset($files[1]); // .. directory if ((count($files) == 1) && is_dir($tmp_location . '/' . current($files))) { $tmp_location .= '/' . current($files); } } $success = drush_move_dir($tmp_location, $download_location, TRUE); // Remove the tarball. if (file_exists($filename)) { drush_delete_dir($filename, TRUE); } } else { // If this is an individual file, and no filename has been specified, // assume the original name. if (is_file($filename) && !$name) { $name = basename($filename); } // The destination directory has already been created by // findDownloadLocation(). $destination = $download_location . ($name ? '/' . $name : ''); $success = drush_move_dir($filename, $destination, TRUE); } return $success ? $download_location : FALSE; } /** * Move a downloaded and unpacked file or directory into place. */ function _make_download_file_move($tmp_path, $filename, $download_location, $subtree = NULL) { $lines = drush_scan_directory($tmp_path, '/./', array('.', '..'), 0, FALSE, 'filename', 0, TRUE); $main_directory = basename($download_location); if (count($lines) == 1) { $directory = array_shift($lines); if ($directory->basename != $main_directory) { drush_move_dir($directory->filename, $tmp_path . DIRECTORY_SEPARATOR . $main_directory, TRUE); } drush_copy_dir($tmp_path . DIRECTORY_SEPARATOR . $main_directory . DIRECTORY_SEPARATOR . $subtree, $download_location, FILE_EXISTS_OVERWRITE); drush_delete_dir($tmp_path, TRUE); } elseif (count($lines) > 1) { drush_delete_dir($download_location, TRUE); drush_move_dir($tmp_path . DIRECTORY_SEPARATOR . $subtree, $download_location, TRUE); } // Remove the tarball. if (file_exists($filename)) { drush_delete_dir($filename, TRUE); } if (file_exists($tmp_path)) { drush_delete_dir($tmp_path, TRUE); } return TRUE; } /** * For backwards compatibility. */ function make_download_get($name, $type, $download, $download_location) { return make_download_file($name, $type, $download, $download_location); } /** * Copies a folder the specified location. * * @return mixed * The TRUE on success, FALSE on failure. */ function make_download_copy($name, $type, $download, $download_location) { if ($folder = _make_download_copy($download['url'])) { drush_log(dt('@project copied from @url.', array('@project' => $name, '@url' => $download['url'])), LogLevel::OK); return drush_copy_dir($folder, $download_location, FILE_EXISTS_OVERWRITE); } make_error('COPY_ERROR', dt('Unable to copy @project from @url.', array('@project' => $name, '@url' => $download['url']))); return FALSE; } /** * Wrapper to drush_download_copy(). * * @param string $folder * The location of the folder to copy. * * @return string * The location of the folder, or FALSE on failure. */ function _make_download_copy($folder) { if (substr($folder, 0, 7) == 'file://') { $folder = substr($folder, 7); } if (is_dir($folder)) { return $folder; } return FALSE; } /** * Checks out a git repository to the specified download location. * * Allowed parameters in $download, in order of precedence: * - 'tag' * - 'revision' * - 'branch' * * This will also attempt to write out release information to the * .info file if the 'no-gitinfofile' option is FALSE. If * $download['full_version'] is present, this will be used, otherwise, * version will be set in this order of precedence: * - 'tag' * - 'branch' * - 'revision' * * @return mixed * The download location on success, FALSE otherwise. */ function make_download_git($name, $type, $download, $download_location) { $tmp_path = make_tmp(); $wc = _get_working_copy_option($download); $checkout_after_clone = TRUE; // If no download URL specified, assume anonymous clone from git.drupal.org. $download['url'] = isset($download['url']) ? $download['url'] : "http://git.drupal.org/project/$name.git"; // If no working-copy download URL specified, assume it is the same. $download['wc_url'] = isset($download['wc_url']) ? $download['wc_url'] : $download['url']; // If not a working copy, and if --no-cache has not been explicitly // declared, create a new git reference cache of the remote repository, // or update the existing cache to fetch recent changes. // @see package_handler_download_project() $cache = !$wc && !drush_get_option('no-cache', FALSE); if ($cache && ($git_cache = drush_directory_cache('git'))) { $project_cache = $git_cache . '/' . $name . '-' . md5($download['url']); // Set up a new cache, if it doesn't exist. if (!file_exists($project_cache)) { $command = 'git clone --mirror'; if (drush_get_context('DRUSH_VERBOSE')) { $command .= ' --verbose --progress'; } $command .= ' %s %s'; drush_shell_cd_and_exec($git_cache, $command, $download['url'], $project_cache); } else { // Update the --mirror clone. drush_shell_cd_and_exec($project_cache, 'git remote update'); } $git_cache = $project_cache; } // Use working-copy download URL if --working-copy specified. $url = $wc ? $download['wc_url'] : $download['url']; $tmp_location = drush_tempdir() . '/' . basename($download_location); $command = 'git clone %s %s'; if (drush_get_context('DRUSH_VERBOSE')) { $command .= ' --verbose --progress'; } if ($cache) { $command .= ' --reference ' . drush_escapeshellarg($git_cache); } // the shallow clone option is only applicable to git entries which reference a tag or a branch if (drush_get_option('shallow-clone', FALSE) && (!empty($download['tag']) || !empty($download['branch']))) { $branch = (!empty($download['branch']) ? $download['branch'] : $download['tag']); $command .= " --depth=1 --branch=${branch}"; // since the shallow copy option automatically "checks out" the requested branch, no further // actions are needed after the clone command $checkout_after_clone = FALSE; } // Before we can checkout anything, we need to clone the repository. if (!drush_shell_exec($command, $url, $tmp_location)) { make_error('DOWNLOAD_ERROR', dt('Unable to clone @project from @url.', array('@project' => $name, '@url' => $url))); return FALSE; } drush_log(dt('@project cloned from @url.', array('@project' => $name, '@url' => $url)), LogLevel::OK); if ($checkout_after_clone) { // Get the current directory (so we can move back later). $cwd = getcwd(); // Change into the working copy of the cloned repo. chdir($tmp_location); // We want to use the most specific target possible, so first try a refspec. if (!empty($download['refspec'])) { if (drush_shell_exec("git fetch %s %s", $url, $download['refspec'])) { drush_log(dt("Fetched refspec !refspec.", array('!refspec' => $download['refspec'])), LogLevel::OK); if (drush_shell_exec("git checkout FETCH_HEAD")) { drush_log(dt("Checked out FETCH_HEAD."), LogLevel::INFO); } } else { make_error('DOWNLOAD_ERROR', dt("Unable to fetch the refspec @refspec from @project.", array('@refspec' => $download['refspec'], '@project' => $name))); } } // If there wasn't a refspec, try a tag. elseif (!empty($download['tag'])) { // @TODO: change checkout to refs path. if (drush_shell_exec("git checkout %s", 'refs/tags/' . $download['tag'])) { drush_log(dt("Checked out tag @tag.", array('@tag' => $download['tag'])), LogLevel::OK); } else { make_error('DOWNLOAD_ERROR', dt("Unable to check out tag @tag.", array('@tag' => $download['tag']))); } } // If there wasn't a tag, try a specific revision hash. elseif (!empty($download['revision'])) { if (drush_shell_exec("git checkout %s", $download['revision'])) { drush_log(dt("Checked out revision @revision.", array('@revision' => $download['revision'])), LogLevel::OK); } else { make_error('DOWNLOAD_ERROR', dt("Unable to checkout revision @revision", array('@revision' => $download['revision']))); } } // If not, see if we at least have a branch. elseif (!empty($download['branch'])) { if (drush_shell_exec("git checkout %s", $download['branch']) && (trim(implode(drush_shell_exec_output())) != '')) { drush_log(dt("Checked out branch @branch.", array('@branch' => $download['branch'])), LogLevel::OK); } elseif (drush_shell_exec("git checkout -b %s %s", $download['branch'], 'origin/' . $download['branch'])) { drush_log(dt('Checked out branch origin/@branch.', array('@branch' => $download['branch'])), LogLevel::OK); } else { make_error('DOWNLOAD_ERROR', dt('Unable to check out branch @branch.', array('@branch' => $download['branch']))); } } if (!empty($download['submodule'])) { $command = 'git submodule update'; foreach ($download['submodule'] as $option) { $command .= ' --%s'; } if (call_user_func_array('drush_shell_exec', array_merge(array($command), $download['submodule']))) { drush_log(dt('Initialized registered submodules.'), LogLevel::OK); } else { make_error('DOWNLOAD_ERROR', dt('Unable to initialize submodules.')); } } // Move back to last current directory (first line). chdir($cwd); } // Move the directory into the final resting location. drush_copy_dir($tmp_location, $download_location, FILE_EXISTS_OVERWRITE); return dirname($tmp_location); } /** * Checks out a Bazaar repository to the specified download location. * * @return mixed * The download location on success, FALSE otherwise. */ function make_download_bzr($name, $type, $download, $download_location) { $tmp_path = make_tmp(); $tmp_location = drush_tempdir() . '/' . basename($download_location); $wc = _get_working_copy_option($download); if (!empty($download['url'])) { $args = array(); $command = 'bzr'; if ($wc) { $command .= ' branch --use-existing-dir'; } else { $command .= ' export'; } if (isset($download['revision'])) { $command .= ' -r %s'; $args[] = $download['revision']; } $command .= ' %s %s'; if ($wc) { $args[] = $download['url']; $args[] = $tmp_location; } else { $args[] = $tmp_location; $args[] = $download['url']; } array_unshift($args, $command); if (call_user_func_array('drush_shell_exec', $args)) { drush_log(dt('@project downloaded from @url.', array('@project' => $name, '@url' => $download['url'])), LogLevel::OK); drush_copy_dir($tmp_location, $download_location, FILE_EXISTS_OVERWRITE); return dirname($download_location); } } else { $download['url'] = dt("unspecified location"); } make_error('DOWNLOAD_ERROR', dt('Unable to download @project from @url.', array('@project' => $name, '@url' => $download['url']))); drush_delete_dir(dirname($tmp_location), TRUE); return FALSE; } /** * Checks out an SVN repository to the specified download location. * * @return mixed * The download location on success, FALSE otherwise. */ function make_download_svn($name, $type, $download, $download_location) { $wc = _get_working_copy_option($download); if (!empty($download['url'])) { if (!empty($download['interactive'])) { $function = 'drush_shell_exec_interactive'; } else { $options = ' --non-interactive'; $function = 'drush_shell_exec'; } if (!isset($download['force']) || $download['force']) { $options = ' --force'; } if ($wc) { $command = 'svn' . $options . ' checkout'; } else { $command = 'svn' . $options . ' export'; } $args = array(); if (isset($download['revision'])) { $command .= ' -r%s'; $args[] = $download['revision']; } $command .= ' %s %s'; $args[] = $download['url']; $args[] = $download_location; if (!empty($download['username'])) { $command .= ' --username %s'; $args[] = $download['username']; if (!empty($download['password'])) { $command .= ' --password %s'; $args[] = $download['password']; } } array_unshift($args, $command); $result = call_user_func_array($function, $args); if ($result) { $args = array( '@project' => $name, '@command' => $command, '@url' => $download['url'], ); drush_log(dt('@project @command from @url.', $args), LogLevel::OK); return $download_location; } else { $download['url'] = dt("unspecified location"); } } else { make_error('DOWNLOAD_ERROR', dt('Unable to download @project from @url.', array('@project' => $name, '@url' => $download['url']))); return FALSE; } } /** * Test that any supplied hash values match the hash of the file content. * * Unsupported hash algorithms are reported as failure. */ function _make_verify_checksums($info, $filename) { $hash_algos = array('md5', 'sha1', 'sha256', 'sha512'); // We only have something to do if a key is an // available function. if (array_intersect(array_keys($info), $hash_algos)) { $content = file_get_contents($filename); foreach ($hash_algos as $algo) { if (!empty($info[$algo])) { $hash = _make_hash($algo, $content); if ($hash !== $info[$algo]) { $args = array( '@algo' => $algo, '@file' => basename($filename), '@expected' => $info[$algo], '@hash' => $hash, ); make_error('DOWNLOAD_ERROR', dt('Checksum @algo verification failed for @file. Expected @expected, received @hash.', $args)); return FALSE; } } } } return TRUE; } /** * Calculate the hash of a string for a given algorithm. */ function _make_hash($algo, $string) { switch ($algo) { case 'md5': return md5($string); case 'sha1': return sha1($string); default: return function_exists('hash') ? hash($algo, $string) : ''; } } 'Restrict the make to this comma-separated list of projects. To specify all projects, pass *.', 'example-value' => 'views,ctools', ); $libraries = array( 'description' => 'Restrict the make to this comma-separated list of libraries. To specify all libraries, pass *.', 'example-value' => 'tinymce', ); $items['make'] = array( 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'description' => 'Turns a makefile into a working Drupal codebase.', 'arguments' => array( 'makefile' => 'Filename of the makefile to use for this build.', 'build path' => 'The path at which to build the makefile.', ), 'examples' => array( 'drush make example.make example' => 'Build the example.make makefile in the example directory.', 'drush make --no-core --contrib-destination=. installprofile.make' => 'Build an installation profile within an existing Drupal site', 'drush make http://example.com/example.make example' => 'Build the remote example.make makefile in the example directory.', 'drush make example.make --no-build --lock=example.lock' => 'Write a new makefile to example.lock. All project versions will be resolved.', ), 'options' => array( 'version' => 'Print the make API version and exit.', 'concurrency' => array( 'description' => 'Set the number of concurrent projects that will be processed at the same time. The default is 1.', 'example-value' => '1', ), 'contrib-destination' => 'Specify a path under which modules and themes should be placed. Defaults to sites/all for Drupal 6,7 and the corresponding directory in the Drupal root for Drupal 8 and above.', 'force-complete' => 'Force a complete build even if errors occur.', 'ignore-checksums' => 'Ignore md5 checksums for downloads.', 'md5' => array( 'description' => 'Output an md5 hash of the current build after completion. Use --md5=print to print to stdout.', 'example-value' => 'print', 'value' => 'optional', ), 'make-update-default-url' => 'The default location to load the XML update information from.', 'no-build' => 'Do not build a codebase. Makes the `build path` argument optional.', 'no-cache' => 'Do not use the pm-download caching (defaults to cache enabled).', 'no-clean' => 'Leave temporary build directories in place instead of cleaning up after completion.', 'no-core' => 'Do not require a Drupal core project to be specified.', 'no-recursion' => 'Do not recurse into the makefiles of any downloaded projects; you can also set [do_recursion] = 0 on a per-project basis in the makefile.', 'no-patch-txt' => 'Do not write a PATCHES.txt file in the directory of each patched project.', 'no-gitinfofile' => 'Do not modify .info files when cloning from Git.', 'force-gitinfofile' => 'Force a modification of .info files when cloning from Git even if repository isn\'t hosted on Drupal.org.', 'no-gitprojectinfo' => 'Do not inject project info into .info files when cloning from Git.', 'overwrite' => 'Overwrite existing directories. Default is to merge.', 'prepare-install' => 'Prepare the built site for installation. Generate a properly permissioned settings.php and files directory.', 'tar' => 'Generate a tar archive of the build. The output filename will be [build path].tar.gz.', 'test' => 'Run a temporary test build and clean up.', 'translations' => 'Retrieve translations for the specified comma-separated list of language(s) if available for all projects.', 'working-copy' => 'Preserves VCS directories, like .git, for projects downloaded using such methods.', 'download-mechanism' => 'How to download files. Should be autodetected, but this is an override if it doesn\'t work. Options are "curl" and "make" (a native download method).', 'projects' => $projects, 'libraries' => $libraries, 'allow-override' => array( 'description' => 'Restrict the make options to a comma-separated list. Defaults to unrestricted.', ), 'lock' => array( 'description' => 'Generate a makefile, based on the one passed in, with all versions *resolved*. Defaults to printing to the terminal, but an output file may be provided.', 'example-value' => 'example.make.lock', ), 'shallow-clone' => array( 'description' => 'For makefile entries which use git for downloading, this option will utilize shallow clones where possible (ie. by using the git-clone\'s depth=1 option). If the "working-copy" option is not desired, this option will significantly speed up makes which involve modules stored in very large git repos. In fact, if "working-copy" option is enabled, this option cannot be used.', ), 'bundle-lockfile' => array( 'description' => 'Generate a lockfile for this build and copy it into the codebase (at sites/all/drush/platform.lock). An alternate path (relative to the Drupal root) can also be specified', 'example-value' => 'sites/all/drush/example.make.lock', ), 'format' => array( 'description' => 'The format for generated lockfiles. Options are "yaml" or "ini". Defaults to "yaml".', 'example-value' => 'ini', ), 'core-quick-drupal' => array( 'description' => 'Return project info for use by core-quick-drupal.', 'hidden' => TRUE, ), 'includes' => 'A list of makefiles to include at build-time.', 'overrides' => 'A list of makefiles to that can override values in other makefiles.', ), 'engines' => array('release_info'), 'topics' => array('docs-make', 'docs-make-example'), ); $items['make-generate'] = array( 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, 'description' => 'Generate a makefile from the current Drupal site.', 'examples' => array( 'drush generate-makefile example.make' => 'Generate a makefile with ALL projects versioned (should a project have a known version number)', 'drush generate-makefile example.make --exclude-versions' => 'Generate a makefile with NO projects versioned', 'drush generate-makefile example.make --exclude-versions=drupal,views,cck' => 'Generate a makefile with ALL projects versioned EXCEPT core, Views and CCK', 'drush generate-makefile example.make --include-versions=admin_menu,og,ctools (--exclude-versions)' => 'Generate a makefile with NO projects versioned EXCEPT Admin Menu, OG and CTools.', ), 'options' => array( 'exclude-versions' => 'Exclude all version numbers (default is include all version numbers) or optionally specify a list of projects to exclude from versioning', 'include-versions' => 'Include a specific list of projects, while all other projects remain unversioned in the makefile (so implies --exclude-versions)', 'format' => array( 'description' => 'The format for generated makefile. Options are "yaml" or "ini". Defaults to "yaml".', 'example-value' => 'ini', ), ), 'engines' => array('release_info'), 'aliases' => array('generate-makefile'), ); $items['make-convert'] = array( 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'description' => 'Convert a legacy makefile into another format. Defaults to converting .make => .make.yml.', 'arguments' => array( 'makefile' => 'Filename of the makefile to convert.', ), 'options' => array( 'projects' => $projects, 'libraries' => $libraries, 'includes' => 'A list of makefiles to include at build-time.', 'format' => 'The format to which the make file should be converted. Accepted values include make, composer, and yml.', ), 'required-arguments' => TRUE, 'examples' => array( 'drush make-convert example.make --format=composer > composer.json' => 'Convert example.make to composer.json', 'drush make-convert example.make --format=yml > example.make.yml' => 'Convert example.make to example.make.yml', 'drush make-convert composer.lock --format=make > example.make' => 'Convert composer.lock example.make', ), ); // Hidden command to build a group of projects. $items['make-process'] = array( 'hidden' => TRUE, 'arguments' => array( 'directory' => 'The temporary working directory to use', ), 'options' => array( 'projects-location' => 'Name of a temporary file containing json-encoded output of make_projects().', 'manifest' => 'An array of projects already being processed.', ), 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'engines' => array('release_info'), ); $items['make-update'] = array( 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'description' => 'Process a makefile and outputs an equivalent makefile with projects version resolved to latest available.', 'arguments' => array( 'makefile' => 'Filename of the makefile to use for this build.', ), 'options' => array( 'result-file' => array( 'description' => 'Save to a file. If not provided, the updated makefile will be dumped to stdout.', 'example-value' => 'updated.make', ), 'format' => array( 'description' => 'The format for generated lockfiles. Options are "yaml" or "ini". Defaults to "yaml".', 'example-value' => 'ini', ), 'includes' => 'A list of makefiles to include at build-time.', ), 'engines' => array('release_info', 'update_status'), ); $items['make-lock'] = array( 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'description' => 'Process a makefile and outputs an equivalent makefile with projects version *resolved*. Respects pinned versions.', 'arguments' => array( 'makefile' => 'Filename of the makefile to use for this build.', ), 'options' => array( 'result-file' => array( 'description' => 'Save to a file. If not provided, the lockfile will be dumped to stdout.', 'example-value' => 'platform.lock', ), 'format' => array( 'description' => 'The format for generated lockfiles. Options are "yaml" or "ini". Defaults to "yaml".', 'example-value' => 'ini', ), 'includes' => 'A list of makefiles to include at build-time.', ), 'allow-additional-options' => TRUE, 'engines' => array('release_info', 'update_status'), ); // Add docs topic. $docs_dir = drush_get_context('DOC_PREFIX', DRUSH_BASE_PATH); $items['docs-make'] = array( 'description' => 'Drush Make overview with examples', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/docs/make.md'), ); $items['docs-make-example'] = array( 'description' => 'Drush Make example makefile', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array($docs_dir . '/examples/example.make.yml'), ); return $items; } /** * Command argument complete callback. * * @return array * Strong glob of files to complete on. */ function make_make_complete() { return array( 'files' => array( 'directories' => array( 'pattern' => '*', 'flags' => GLOB_ONLYDIR, ), 'make' => array( 'pattern' => '*.make', ), ), ); } /** * Validation callback for make command. */ function drush_make_validate($makefile = NULL, $build_path = NULL) { // Don't validate if --version option is supplied. if (drush_get_option('version', FALSE)) { return; } if (drush_get_option('shallow-clone', FALSE) && drush_get_option('working-copy', FALSE)) { return drush_set_error('MAKE_SHALLOW_CLONE_WORKING_COPY_CONFLICT', dt('You cannot use "--shallow-clone" and "--working-copy" options together.')); } // Error out if the build path is not valid and --no-build was not supplied. if (!drush_get_option('no-build', FALSE) && !make_build_path($build_path)) { return FALSE; } } /** * Implements drush_hook_pre_COMMAND(). * * If --version option is supplied, print it and prevent execution of the command. */ function drush_make_pre_make($makefile = NULL, $build_path = NULL) { if (drush_get_option('version', FALSE)) { drush_print(dt('Drush make API version !version', array('!version' => MAKE_API))); drush_print_pipe(MAKE_API); // Prevent command execution. return FALSE; } } /** * Drush callback; make based on the makefile. */ function drush_make($makefile = NULL, $build_path = NULL) { // Set the cache option based on our '--no-cache' option. _make_enable_cache(); // Build. if (!drush_get_option('no-build', FALSE)) { $info = make_parse_info_file($makefile); drush_log(dt('Beginning to build !makefile.', array('!makefile' => $makefile)), LogLevel::OK); // Default contrib destination depends on Drupal core version. $core_version = str_replace('.x', '', $info['core'][0]); $sitewide = drush_drupal_sitewide_directory($core_version); $contrib_destination = drush_get_option('contrib-destination', $sitewide); $build_path = make_build_path($build_path); $make_dir = realpath(dirname($makefile)); $success = make_projects(FALSE, $contrib_destination, $info, $build_path, $make_dir); if ($success) { make_libraries(FALSE, $contrib_destination, $info, $build_path, $make_dir); if (drush_get_option('prepare-install')) { make_prepare_install($build_path); } if ($option = drush_get_option('md5')) { $md5 = make_md5(); if ($option === 'print') { drush_print($md5); } else { drush_log(dt('Build hash: %md5', array('%md5' => $md5)), LogLevel::OK); } } // Only take final build steps if not in testing mode. if (!drush_get_option('test')) { if (drush_get_option('tar')) { make_tar($build_path); } else { make_move_build($build_path); } } make_clean_tmp(); } else { return make_error('MAKE_PROJECTS_FAILED', dt('Drush Make failed to download all projects. See the log above for the specific errors.')); } } // Process --lock and --bundle-lockfile $lockfiles = array(); if ($result_file = drush_get_option('bundle-lockfile', FALSE)) { if ($result_file === TRUE) { $result_file = 'sites/all/drush/platform.make'; } $lockfiles[] = $build_path . '/' . $result_file; } if ($result_file = drush_get_option('lock', FALSE)) { $lockfiles[] = $result_file; } if (count($lockfiles)) { foreach ($lockfiles as $lockfile) { if ($lockfile !== TRUE) { $result_file = drush_normalize_path($lockfile); drush_mkdir(dirname($result_file), $required = TRUE); drush_set_option('result-file', $result_file); } drush_invoke('make-lock', $makefile); drush_unset_option('result-file'); } } // Used by core-quick-drupal command. // @see drush_core_quick_drupal(). if (drush_get_option('core-quick-drupal', FALSE)) { return $info; } } /** * Command callback; convert ini makefile to YAML. */ function drush_make_convert($source) { $dest_format = drush_get_option('format', 'yml'); // Load source data. $source_format = pathinfo($source, PATHINFO_EXTENSION); if ($source_format == $dest_format || $source_format == 'lock' && $dest_format == 'composer') { drush_print('The source format cannot be the same as the destination format.'); } // Obtain drush make $info array, converting if necessary. switch ($source_format) { case 'make': case 'yml': case 'yaml': $info = make_parse_info_file($source); break; case 'lock': $composer_json_file = str_replace('lock', 'json', $source); if (!file_exists($composer_json_file)) { drush_print('Please ensure that a composer.json file is in the same directory as the specified composer.lock file.'); return FALSE; } $composer_json = json_decode(make_get_data($composer_json_file), TRUE); $composer_lock = json_decode(make_get_data($source), TRUE); $info = drush_make_convert_composer_to_make($composer_lock, $composer_json); break; case 'json': drush_print('Please use composer.lock instead of composer.json as source for conversion.'); return FALSE; break; } // Output into destination formation. switch ($dest_format) { case 'yml': case 'yaml': $output = drush_make_convert_make_to_yml($info); break; case 'make': foreach ($info['projects'] as $key => $project) { $info['projects'][$key]['_type'] = $info['projects'][$key]['type']; } foreach ($info['libraries'] as $key => $library) { $info['libraries'][$key]['_type'] = 'librarie'; } $output = _drush_make_generate_makefile_contents($info['projects'], $info['libraries'], $info['core'], $info['defaults']); break; case 'composer': $output = drush_make_convert_make_to_composer($info); break; } drush_print($output); } /** * Converts a composer.lock array into a traditional drush make array. * * @param array $composer_lock * An array of composer.lock data. * * @param array $composer_json * An array of composer.json data. * * @return array A traditional drush make info array. * A traditional drush make info array. */ function drush_make_convert_composer_to_make($composer_lock, $composer_json) { $info = array( 'core' => array(), 'api' => 2, 'defaults' => array( 'projects' => array( 'subdir' => 'contrib', ), ), 'projects' => array(), 'libraries' => array(), ); // The make generation function requires that projects be grouped by type, // or else duplicative project groups will be created. $core = array(); $modules = array(); $themes = array(); $profiles = array(); $libraries = array(); foreach ($composer_lock['packages'] as $key => $package) { if (strpos($package['name'], 'drupal/') === 0 && in_array($package['type'], array('drupal-core', 'drupal-theme', 'drupal-module', 'drupal-profile'))) { $project_name = str_replace('drupal/', '', $package['name']); switch ($package['type']) { case 'drupal-core': $project_name = 'drupal'; $group =& $core; $group[$project_name]['type'] = 'core'; $info['core'] = substr($package['version'], 0, 1) . '.x'; break; case 'drupal-theme': $group =& $themes; $group[$project_name]['type'] = 'theme'; break; case 'drupal-module': $group =& $modules; $group[$project_name]['type'] = 'module'; break; case 'drupal-profile': $group =& $profiles; $group[$project_name]['type'] = 'profile'; break; } $group[$project_name]['download']['type'] = 'git'; $group[$project_name]['download']['url'] = $package['source']['url']; // Dev versions should use git branch + revision, otherwise a tag is used. if (strstr($package['version'], 'dev')) { // 'dev-' prefix indicates a branch-alias. Stripping the dev prefix from // the branch name is sufficient. // @see https://getcomposer.org/doc/articles/aliases.md if (strpos($package['version'], 'dev-') === 0) { $group[$project_name]['download']['branch'] = substr($package['version'], 4); } // Otherwise, leave as is. Version may already use '-dev' suffix. else { $group[$project_name]['download']['branch'] = $package['version']; } $group[$project_name]['download']['revision'] = $package['source']['reference']; } elseif ($package['type'] == 'drupal-core') { // For 7.x tags, replace 7.xx.0 with 7.xx. if ($info['core'] == '7.x') { $group[$project_name]['download']['tag']= substr($package['version'], 0, 4); } else { $group[$project_name]['download']['tag'] = $package['version']; } } else { // Make tag versioning drupal-friendly. 8.1.0-alpha1 => 8.x-1.0-alpha1. $major_version = substr($package['version'], 0 ,1); $the_rest = substr($package['version'], 2, strlen($package['version'])); $group[$project_name]['download']['tag'] = "$major_version.x-$the_rest"; } if (!empty($package['extra']['patches_applied'])) { foreach ($package['extra']['patches_applied'] as $desc => $url) { $group[$project_name]['patch'][] = $url; } } } // Include any non-drupal libraries that exist in both .lock and .json. elseif (!in_array($package['type'], array('composer-plugin', 'metapackage')) && array_key_exists($package['name'], $composer_json['require'])) { $project_name = $package['name']; $libraries[$project_name]['type'] = 'library'; $libraries[$project_name]['download']['type'] = 'git'; $libraries[$project_name]['download']['url'] = $package['source']['url']; $libraries[$project_name]['download']['branch'] = $package['version']; $libraries[$project_name]['download']['revision'] = $package['source']['reference']; } } $info['projects'] = $core + $modules + $themes; $info['libraries'] = $libraries; return $info; } /** * Converts a drush info array to a composer.json array. * * @param array $info * A drush make info array. * * @return string * A json encoded composer.json schema object. */ function drush_make_convert_make_to_composer($info) { $core_major_version = substr($info['core'], 0, 1); $core_project_name = $core_major_version == 7 ? 'drupal/drupal' : 'drupal/core'; // Add default projects. $projects = array( 'composer/installers' => '^1.0.20', 'cweagans/composer-patches' => '~1.0', $core_project_name => str_replace('x', '*', $info['core']), ); $patches = array(); // Iterate over projects, populating composer-friendly array. foreach ($info['projects'] as $project_name => $project) { switch ($project['type']) { case 'core': $project['name'] = 'drupal/core'; $projects[$project['name']] = str_replace('x', '*', $project['version']); break; default: $project['name'] = "drupal/$project_name"; $projects[$project['name']] = drush_make_convert_project_to_composer($project, $core_major_version); break; } // Add project patches. if (!empty($project['patch'])) { foreach($project['patch'] as $key => $patch) { $patch_description = "Enter {$project['name']} patch #$key description here"; $patches[$project['name']][$patch_description] = $patch; } } } // Iterate over libraries, populating composer-friendly array. if (!empty($info['libraries'])) { foreach ($info['libraries'] as $library_name => $library) { $library_name = 'Verify project name: ' . $library_name; $projects[$library_name] = drush_make_convert_project_to_composer($library, $core_major_version); } } $output = array( 'name' => 'Enter project name here', 'description' => 'Enter project description here', 'type' => 'project', 'repositories' => array( array('type' => 'composer', 'url' => 'https://packagist.drupal-composer.org'), ), 'require' => $projects, 'minimum-stability' => 'dev', 'prefer-stable' => TRUE, 'extra' => array( 'installer-paths' => array( 'core' => array('type:drupal-core'), 'docroot/modules/contrib/{$name}' => array('type:drupal-module'), 'docroot/profiles/contrib/{$name}' => array('type:drupal-profile'), 'docroot/themes/contrib/{$name}' => array('type:drupal-theme'), 'drush/contrib/{$name}' => array('type:drupal-drush'), ), 'patches' => $patches, ), ); $output = json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); return $output; } /** * Converts a make file project array into a composer project version string. * * @param array $original_project * A project dependency, as defined in a make file. * * @param string $core_major_version * The major core version. E.g., 6, 7, 8, etc. * * @return string * The project version, in composer syntax. * */ function drush_make_convert_project_to_composer($original_project, $core_major_version) { // Typical specified version with major version "x" removed. if (!empty($original_project['version'])) { $version = str_replace('x', '0', $original_project['version']); } // Git branch or revision. elseif (!empty($original_project['download'])) { switch ($original_project['download']['type']) { case 'git': if (!empty($original_project['download']['branch'])) { // @todo Determine if '0' will always be correct. $version = str_replace('x', '0', $original_project['download']['branch']); } if (!empty($original_project['download']['tag'])) { // @todo Determine if '0' will always be correct. $version = str_replace('x', '0', $original_project['download']['tag']); } if (!empty($project['download']['revision'])) { $version .= '#' . $original_project['download']['revision']; } break; default: $version = 'Enter correct project name and version number'; break; } } $version = "$core_major_version." . $version; return $version; } /** * Converts a drush info array to a YAML array. * * @param array $info * A drush make info array. * * @return string * A yaml encoded info array. */ function drush_make_convert_make_to_yml($info) { // Remove incorrect value. unset($info['format']); // Replace "*" with "~" for project versions. foreach ($info['projects'] as $key => $project) { if ($project['version'] == '*') { $info['projects'][$key]['version'] = '~'; } } $dumper = drush_load_engine('outputformat', 'yaml'); $output = $dumper->format($info, array()); return $output; } /** * Drush callback: hidden file to process an individual project. * * @param string $directory * Directory where the project is being built. */ function drush_make_process($directory) { drush_get_engine('release_info'); // Set the temporary directory. make_tmp(TRUE, $directory); if (!$projects_location = drush_get_option('projects-location')) { return drush_set_error('MAKE-PROCESS', dt('No projects passed to drush_make_process')); } $projects = json_decode(file_get_contents($projects_location), TRUE); $manifest = drush_get_option('manifest', array()); foreach ($projects as $project) { if ($instance = DrushMakeProject::getInstance($project['type'], $project)) { $instance->setManifest($manifest); $instance->make(); } else { make_error('PROJECT-TYPE', dt('Non-existent project type %type on project %project.', array('%type' => $project['type'], '%project' => $project['name']))); } } } /** * Gather additional data on all projects specified in the make file. */ function make_prepare_projects($recursion, $info, $contrib_destination = '', $build_path = '', $make_dir = '') { $release_info = drush_get_engine('release_info'); // Nothing to make if the project list is empty. Maybe complain about it. if (empty($info['projects'])) { if (drush_get_option('no-core') || $recursion) { return TRUE; } else { return drush_set_error('MAKE_NO_CORE', dt('No core project specified.')); } } // Obtain translations to download along with the projects. $translations = array(); if (isset($info['translations'])) { $translations = $info['translations']; } if ($arg_translations = drush_get_option('translations', FALSE)) { $translations = array_merge(explode(',', $arg_translations), $translations); } // Normalize projects. $projects = array(); $ignore_checksums = drush_get_option('ignore-checksums'); foreach ($info['projects'] as $key => $project) { // Merge the known data onto the project info. $project += array( 'name' => $key, 'type' => 'module', 'core' => $info['core'], 'translations' => $translations, 'build_path' => $build_path, 'contrib_destination' => $contrib_destination, 'version' => '', 'location' => drush_get_option('make-update-default-url', ReleaseInfo::DEFAULT_URL), 'subdir' => '', 'directory_name' => '', 'make_directory' => $make_dir, 'options' => array(), ); // MD5 Checksum. if ($ignore_checksums) { unset($project['download']['md5']); } elseif (!empty($project['md5'])) { $project['download']['md5'] = $project['md5']; } // If download components are specified, but not the download // type, default to git. if (isset($project['download']) && !isset($project['download']['type'])) { $project['download']['type'] = 'git'; } // Localization server. if (!isset($project['l10n_url']) && ($project['location'] == ReleaseInfo::DEFAULT_URL)) { $project['l10n_url'] = MAKE_DEFAULT_L10N_SERVER; } // Classify projects in core or contrib. if ($project['type'] == 'core') { $project['download_type'] = 'core'; } elseif ($project['location'] != ReleaseInfo::DEFAULT_URL || !isset($project['download'])) { $request = make_prepare_request($project); $is_core = $release_info->checkProject($request, 'core'); $project['download_type'] = ($is_core ? 'core' : 'contrib'); $project['type'] = $is_core ? 'core' : $project['type']; } else { $project['download_type'] = ($project['name'] == 'drupal' ? 'core' : 'contrib'); } $projects[$project['download_type']][$project['name']] = $project; } // Verify there're enough cores, but not too many. $cores = !empty($projects['core']) ? count($projects['core']) : 0; if (drush_get_option('no-core')) { unset($projects['core']); } elseif ($cores == 0 && !$recursion) { return drush_set_error('MAKE_NO_CORE', dt('No core project specified.')); } elseif ($cores == 1 && $recursion) { unset($projects['core']); } elseif ($cores > 1) { return drush_set_error('MAKE_MULTIPLE_CORES', dt('More than one core project specified.')); } // Set download type = pm for suitable projects. foreach (array_keys($projects) as $project_type) { foreach ($projects[$project_type] as $project) { if (make_project_needs_release_info($project)) { $request = make_prepare_request($project, $project_type); $release = $release_info->selectReleaseBasedOnStrategy($request, '', 'ignore'); if ($release === FALSE) { return FALSE; } // Override default project type with data from update service. if (!isset($info['projects'][$project['name']]['type'])) { $project['type'] = $release_info->get($request)->getType(); } if (!isset($project['download'])) { $project['download'] = array( 'type' => 'pm', 'full_version' => $release['version'], 'download_link' => $release['download_link'], 'status url' => $request['status url'], ); } } $projects[$project_type][$project['name']] = $project; } } if (!$recursion) { $projects += array( 'core' => array(), 'contrib' => array(), ); drush_set_option('DRUSH_MAKE_PROJECTS', array_merge($projects['core'], $projects['contrib'])); } return $projects; } /** * Process all projects specified in the make file. */ function make_projects($recursion, $contrib_destination, $info, $build_path, $make_dir) { $projects = make_prepare_projects($recursion, $info, $contrib_destination, $build_path, $make_dir); // Abort if there was an error processing projects. if ($projects === FALSE) { return FALSE; } // Core is built in place, rather than using make-process. if (!empty($projects['core']) && count($projects['core'])) { $project = current($projects['core']); $project = DrushMakeProject::getInstance('core', $project); $project->make(); } // Process all projects concurrently using make-process. if (isset($projects['contrib'])) { $concurrency = drush_get_option('concurrency', 1); // Generate $concurrency sub-processes to do the actual work. $invocations = array(); $thread = 0; foreach ($projects['contrib'] as $project) { $thread = ++$thread % $concurrency; // Ensure that we've set this sub-process up. if (!isset($invocations[$thread])) { $invocations[$thread] = array( 'args' => array( make_tmp(), ), 'options' => array( 'projects' => array(), ), 'site' => array(), ); } // Add the project to this sub-process. $invocations[$thread]['options']['projects'][] = $project; // Add the manifest so recursive downloads do not override projects. $invocations[$thread]['options']['manifest'] = array_keys($projects['contrib']); } if (!empty($invocations)) { // Backend options. $backend_options = array( 'concurrency' => $concurrency, 'method' => 'POST', ); // Store projects in temporary files since passing this much data on the // pipe buffer can break on certain systems. _make_write_project_json($invocations); $common_options = drush_redispatch_get_options(); // Merge in stdin options since we process makefiles recursively. See http://drupal.org/node/1510180. $common_options = array_merge($common_options, drush_get_context('stdin')); // Package handler should use 'wget'. $common_options['package-handler'] = 'wget'; // Avoid any prompts from CLI. $common_options['yes'] = TRUE; // Use cache unless explicitly turned off. if (!drush_get_option('no-cache', FALSE)) { $common_options['cache'] = TRUE; } // Unless --verbose or --debug are passed, quiter backend output. if (empty($common_options['verbose']) && empty($common_options['debug'])) { $backend_options['#output-label'] = FALSE; $backend_options['integrate'] = TRUE; } $results = drush_backend_invoke_concurrent($invocations, $common_options, $backend_options, 'make-process', '@none'); if (count($results['error_log'])) { return FALSE; } } } return TRUE; } /** * Writes out project data to temporary files. * * @param array &$invocations * An array containing projects sorted by thread. */ function _make_write_project_json(array &$invocations) { foreach ($invocations as $thread => $info) { $projects = $info['options']['projects']; unset($invocations[$thread]['options']['projects']); $temp_file = drush_tempnam('make_projects'); file_put_contents($temp_file, json_encode($projects)); $invocations[$thread]['options']['projects-location'] = $temp_file; } } /** * Gather additional data on all libraries specified in the make file. */ function make_prepare_libraries($recursion, $info, $contrib_destination = '', $build_path = '', $make_dir = '') { // Nothing to make if the libraries list is empty. if (empty($info['libraries'])) { return; } $libraries = array(); $ignore_checksums = drush_get_option('ignore-checksums'); foreach ($info['libraries'] as $key => $library) { if (!is_string($key) || !is_array($library)) { // TODO Print a prettier message. continue; } // Merge the known data onto the library info. $library += array( 'name' => $key, 'core' => $info['core'], 'build_path' => $build_path, 'contrib_destination' => $contrib_destination, 'subdir' => '', 'directory_name' => $key, 'make_directory' => $make_dir, ); if ($ignore_checksums) { unset($library['download']['md5']); } $libraries[$key] = $library; } if (!$recursion) { drush_set_option('DRUSH_MAKE_LIBRARIES', $info['libraries']); } return $libraries; } /** * Process all libraries specified in the make file. */ function make_libraries($recursion, $contrib_destination, $info, $build_path, $make_dir) { $libraries = make_prepare_libraries($recursion, $info, $contrib_destination, $build_path, $make_dir); if (empty($libraries)) { return; } foreach ($libraries as $key => $library) { $class = DrushMakeProject::getInstance('library', $library); $class->make(); } } /** * The path where the final build will be placed. */ function make_build_path($build_path) { static $saved_path; if (isset($saved_path)) { return $saved_path; } // Determine the base of the build. if (drush_get_option('tar')) { $build_path = dirname($build_path) . '/' . basename($build_path, '.tar.gz') . '.tar.gz'; } elseif (isset($build_path) && (!empty($build_path) || $build_path == '.')) { $build_path = rtrim($build_path, '/'); } // Allow tests to run without a specified base path. elseif (drush_get_option('test') || drush_confirm(dt("Make new site in the current directory?"))) { $build_path = '.'; } else { return drush_user_abort(dt('Build aborted.')); } if ($build_path != '.' && file_exists($build_path) && !drush_get_option('no-core', FALSE)) { return drush_set_error('MAKE_PATH_EXISTS', dt('Base path %path already exists.', array('%path' => $build_path))); } $saved_path = $build_path; return $build_path; } /** * Move the completed build into place. */ function make_move_build($build_path) { $tmp_path = make_tmp(); $ret = TRUE; if ($build_path == '.' || (drush_get_option('no-core', FALSE) && file_exists($build_path))) { $info = drush_scan_directory($tmp_path . DIRECTORY_SEPARATOR . '__build__', '/./', array('.', '..'), 0, FALSE, 'filename', 0, TRUE); foreach ($info as $file) { $destination = $build_path . DIRECTORY_SEPARATOR . $file->basename; if (file_exists($destination)) { // To prevent the removal of top-level directories such as 'modules' or // 'themes', descend in a level if the file exists. // TODO: This only protects one level of directories from being removed. $overwrite = drush_get_option('overwrite', FALSE) ? FILE_EXISTS_OVERWRITE : FILE_EXISTS_MERGE; if (is_dir($destination)) { $files = drush_scan_directory($file->filename, '/./', array('.', '..'), 0, FALSE); foreach ($files as $file) { $ret = $ret && drush_copy_dir($file->filename, $destination . DIRECTORY_SEPARATOR . $file->basename, $overwrite); } } else { $ret = $ret && drush_copy_dir($file->filename, $destination, $overwrite); } } else { $ret = $ret && drush_copy_dir($file->filename, $destination); } } } else { drush_mkdir(dirname($build_path)); $ret = drush_move_dir($tmp_path . DIRECTORY_SEPARATOR . '__build__', $tmp_path . DIRECTORY_SEPARATOR . basename($build_path), TRUE); $ret = $ret && drush_copy_dir($tmp_path . DIRECTORY_SEPARATOR . basename($build_path), $build_path); } // Copying to final destination resets write permissions. Re-apply. if (drush_get_option('prepare-install')) { $default = $build_path . '/sites/default'; chmod($default . '/settings.php', 0666); chmod($default . '/files', 0777); } if (!$ret) { return drush_set_error('MAKE_CANNOT_MOVE_BUILD', dt("Cannot move build into place.")); } return $ret; } /** * Create a request array suitable for release_info engine. * * This is a convenience function to easily integrate drush_make * with drush release_info engine. * * @todo: refactor 'make' to internally work with release_info keys. * * @param array $project * Project array. * @param string $type * 'contrib' or 'core'. */ function make_prepare_request($project, $type = 'contrib') { $request = array( 'name' => $project['name'], 'drupal_version' => $project['core'], 'status url' => $project['location'], ); if ($project['version'] != '') { $request['project_version'] = $project['version']; $request['version'] = $type == 'core' ? $project['version'] : $project['core'] . '-' . $project['version']; } return $request; } /** * Determine if the release information is required for this * project. When it is determined that it is, this potentially results * in the use of pm-download to process the project. * * If the location of the project is not customized (uses d.o), and * one of the following is true, then release information is required: * * - $project['type'] has not been specified * - $project['download'] has not been specified * * @see make_projects() */ function make_project_needs_release_info($project) { return isset($project['location']) // Only fetch release info if the project type is unknown OR if // download attributes are unspecified. && (!isset($project['type']) || !isset($project['download'])); } /** * Enables caching if not explicitly disabled. * * @return bool * The previous value of the 'cache' option. */ function _make_enable_cache() { $cache_before = drush_get_option('cache'); if (!drush_get_option('no-cache', FALSE)) { drush_set_option('cache', TRUE); } return $cache_before; } $value) { $this->{$key} = $value; } if (!empty($this->options['working-copy'])) { $this->download['working-copy'] = TRUE; } // Don't recurse when we're using a pre-built profile tarball. if ($this->variant == 'projects') { $this->do_recursion = FALSE; } } /** * Get an instance for the type and project. * * @param string $type * Type of project: core, library, module, profile, or translation. * @param array $project * Project information. * * @return mixed * An instance for the project or FALSE if invalid type. */ public static function getInstance($type, $project) { if (!isset(self::$self[$type][$project['name']])) { $class = 'DrushMakeProject_' . $type; self::$self[$type][$project['name']] = class_exists($class) ? new $class($project) : FALSE; } return self::$self[$type][$project['name']]; } /** * Set the manifest array. * * @param array $manifest * An array of projects as generated by `make_projects`. */ public function setManifest($manifest) { $this->manifest = $manifest; } /** * Download a project. */ function download() { $this->downloaded = TRUE; // In some cases, make_download_factory() is going to need to know the // full version string of the project we're trying to download. However, // the version is a project-level attribute, not a download-level // attribute. So, if we don't already have a full version string in the // download array (e.g. if it was initialized via the release history XML // for the PM case), we take the version info from the project-level // attribute, convert it into a full version string, and stuff it into // $this->download so that the download backend has access to it, too. if (!empty($this->version) && empty($this->download['full_version'])) { $full_version = ''; $matches = array(); // Core needs different conversion rules than contrib. if (!empty($this->type) && $this->type == 'core') { // Generally, the version for core is already set properly. $full_version = $this->version; // However, it might just be something like '7' or '7.x', in which // case we need to turn that into '7.x-dev'; if (preg_match('/^\d+(\.x)?$/', $this->version, $matches)) { // If there's no '.x' already, append it. if (empty($matches[1])) { $full_version .= '.x'; } $full_version .= '-dev'; } } // Contrib. else { // If the version doesn't already define a core version, prepend it. if (!preg_match('/^\d+\.x-\d+.*$/', $this->version)) { // Just find the major version from $this->core so we don't end up // with version strings like '7.12-2.0'. $core_parts = explode('.', $this->core); $full_version = $core_parts[0] . '.x-'; } $full_version .= $this->version; // If the project-level version attribute is just a number it's a major // version. if (preg_match('/^\d+(\.x)?$/', $this->version, $matches)) { // If there's no '.x' already, append it. if (empty($matches[1])) { $full_version .= '.x'; } $full_version .= '-dev'; } } $this->download['full_version'] = $full_version; } $this->download['variant'] = $this->variant; if (make_download_factory($this->name, $this->type, $this->download, $this->download_location) === FALSE) { $this->downloaded = FALSE; } return $this->downloaded; } /** * Build a project. */ function make() { if ($this->made) { drush_log(dt('Attempt to build project @project more then once prevented.', array('@project' => $this->name))); return TRUE; } $this->made = TRUE; if (!isset($this->download_location)) { $this->download_location = $this->findDownloadLocation(); } if ($this->download() === FALSE) { return FALSE; } if (!$this->addLockfile($this->download_location)) { return FALSE; } if (!$this->applyPatches($this->download_location)) { return FALSE; } if (!$this->getTranslations($this->download_location)) { return FALSE; } // Handle .info file re-writing (if so desired). if (!drush_get_option('no-gitinfofile', FALSE) && isset($this->download['type']) && $this->download['type'] == 'git') { $this->processGitInfoFiles(); } // Clean-up .git directories. if (!_get_working_copy_option($this->download)) { $this->removeGitDirectory(); } if (!$this->recurse($this->download_location)) { return FALSE; } return TRUE; } /** * Determine the location to download project to. */ function findDownloadLocation() { $this->path = $this->generatePath(); $this->project_directory = !empty($this->directory_name) ? $this->directory_name : $this->name; $this->download_location = $this->path . '/' . $this->project_directory; // This directory shouldn't exist yet -- if it does, stop, // unless overwrite has been set to TRUE. if (is_dir($this->download_location) && !$this->overwrite) { return drush_set_error('MAKE_DIRECTORY_EXISTS', dt('Directory not empty: !directory', array('!directory' => $this->download_location))); } elseif ($this->download['type'] === 'pm') { // pm-download will create the final contrib directory. drush_mkdir(dirname($this->download_location)); } else { drush_mkdir($this->download_location); } return $this->download_location; } /** * Rewrite relative URLs and file:/// URLs * * relative path -> absolute path using the make_directory * local file:/// urls -> local paths * * @param mixed &$info * Either an array or a simple url string. The `$info` variable will be * transformed into an array. */ protected function preprocessLocalFileUrl(&$info) { if (is_string($info)) { $info = array('url' => $info, 'local' => FALSE); } if (!_drush_is_url($info['url']) && !drush_is_absolute_path($info['url'])) { $info['url'] = $this->make_directory . '/' . $info['url']; $info['local'] = TRUE; } elseif (substr($info['url'], 0, 8) == 'file:///') { $info['url'] = substr($info['url'], 7); $info['local'] = TRUE; } } /** * Retrieve and apply any patches specified by the makefile to this project. */ function applyPatches($project_directory) { if (empty($this->patch)) { return TRUE; } $patches_txt = ''; $local_patches = array(); $ignore_checksums = drush_get_option('ignore-checksums'); foreach ($this->patch as $info) { $this->preprocessLocalFileUrl($info); // Download the patch. if ($filename = _make_download_file($info['url'])) { $patched = FALSE; $output = ''; // Test each patch style; -p1 is the default with git. See // http://drupal.org/node/1054616 $patch_levels = array('-p1', '-p0'); foreach ($patch_levels as $patch_level) { $checked = drush_shell_cd_and_exec($project_directory, 'git --git-dir=. apply --check %s %s --verbose', $patch_level, $filename); if ($checked) { // Apply the first successful style. $patched = drush_shell_cd_and_exec($project_directory, 'git --git-dir=. apply %s %s --verbose', $patch_level, $filename); break; } } // In some rare cases, git will fail to apply a patch, fallback to using // the 'patch' command. if (!$patched) { foreach ($patch_levels as $patch_level) { // --no-backup-if-mismatch here is a hack that fixes some // differences between how patch works on windows and unix. if ($patched = drush_shell_exec("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $project_directory, $filename)) { break; } } } if ($output = drush_shell_exec_output()) { // Log any command output, visible only in --verbose or --debug mode. drush_log(implode("\n", $output)); } // Set up string placeholders to pass to dt(). $dt_args = array( '@name' => $this->name, '@filename' => basename($filename), ); if ($patched) { if (!$ignore_checksums && !_make_verify_checksums($info, $filename)) { return FALSE; } $patch_url = $info['url']; // If this is a local patch, copy that into place as well. if ($info['local']) { $local_patches[] = $info['url']; // Use a local path for the PATCHES.txt file. $pathinfo = pathinfo($patch_url); $patch_url = $pathinfo['basename']; } $patches_txt .= '- ' . $patch_url . "\n"; drush_log(dt('@name patched with @filename.', $dt_args), LogLevel::OK); } else { make_error('PATCH_ERROR', dt("Unable to patch @name with @filename.", $dt_args)); } drush_op('unlink', $filename); } else { make_error('DOWNLOAD_ERROR', 'Unable to download ' . $info['url'] . '.'); return FALSE; } } if (!empty($patches_txt) && !drush_get_option('no-patch-txt') && !file_exists($project_directory . '/PATCHES.txt')) { $patches_txt = "The following patches have been applied to this project:\n" . $patches_txt . "\nThis file was automatically generated by Drush Make (http://drupal.org/project/drush).\n"; file_put_contents($project_directory . '/PATCHES.txt', $patches_txt); drush_log('Generated PATCHES.txt file for ' . $this->name, LogLevel::OK); // Copy local patches into place. foreach ($local_patches as $url) { $pathinfo = pathinfo($url); drush_copy_dir($url, $project_directory . '/' . $pathinfo['basename']); } } return TRUE; } /** * Process info files when downloading things from git. */ function processGitInfoFiles() { // Bail out if this isn't hosted on Drupal.org (unless --force-gitinfofile option was specified). if (!drush_get_option('force-gitinfofile', FALSE) && isset($this->download['url']) && strpos($this->download['url'], 'drupal.org') === FALSE) { return; } // Figure out the proper version string to use based on the .make file. // Best case is the .make file author told us directly. if (!empty($this->download['full_version'])) { $full_version = $this->download['full_version']; } // Next best is if we have a tag, since those are identical to versions. elseif (!empty($this->download['tag'])) { $full_version = $this->download['tag']; } // If we have a branch, append '-dev'. elseif (!empty($this->download['branch'])) { $full_version = $this->download['branch'] . '-dev'; } // Ugh. Not sure what else we can do in this case. elseif (!empty($this->download['revision'])) { $full_version = $this->download['revision']; } // Probably can never reach this case. else { $full_version = 'unknown'; } // If the version string ends in '.x-dev' do the Git magic to figure out // the appropriate 'rebuild version' string, e.g. '7.x-1.2+7-dev'. $matches = array(); if (preg_match('/^(.+).x-dev$/', $full_version, $matches)) { require_once dirname(__FILE__) . '/../pm/package_handler/git_drupalorg.inc'; $rebuild_version = drush_pm_git_drupalorg_compute_rebuild_version($this->download_location, $matches[1]); if ($rebuild_version) { $full_version = $rebuild_version; } } require_once dirname(__FILE__) . '/../pm/pm.drush.inc'; if (drush_shell_cd_and_exec($this->download_location, 'git log -1 --pretty=format:%ct')) { $output = drush_shell_exec_output(); $datestamp = $output[0]; } else { $datestamp = time(); } drush_pm_inject_info_file_metadata($this->download_location, $this->name, $full_version, $datestamp); } /** * Remove the .git directory from a project. */ function removeGitDirectory() { if (isset($this->download['type']) && $this->download['type'] == 'git' && file_exists($this->download_location . '/.git')) { drush_delete_dir($this->download_location . '/.git', TRUE); } } /** * Add a lock file. */ function addLockfile($project_directory) { if (!empty($this->lock)) { file_put_contents($project_directory . '/.drush-lock-update', $this->lock); } return TRUE; } /** * Retrieve translations for this project. */ function getTranslations($project_directory) { static $cache = array(); $langcodes = $this->translations; if ($langcodes && in_array($this->type, array('core', 'module', 'profile', 'theme'), TRUE)) { // Support the l10n_path, l10n_url keys from l10n_update. Note that the // l10n_server key is not supported. if (isset($this->l10n_path)) { $update_url = $this->l10n_path; } else { if (isset($this->l10n_url)) { $l10n_server = $this->l10n_url; } else { $l10n_server = FALSE; } if ($l10n_server) { if (!isset($cache[$l10n_server])) { $this->preprocessLocalFileUrl($l10n_server); $l10n_server = $l10n_server['url']; if ($filename = _make_download_file($l10n_server)) { $server_info = simplexml_load_string(file_get_contents($filename)); $cache[$l10n_server] = !empty($server_info->update_url) ? $server_info->update_url : FALSE; } } if ($cache[$l10n_server]) { $update_url = $cache[$l10n_server]; } else { make_error('XML_ERROR', dt("Could not retrieve l10n update url for !project.", array('!project' => $this->name))); return FALSE; } } } if ($update_url) { $failed = array(); foreach ($langcodes as $langcode) { $variables = array( '%project' => $this->name, '%release' => $this->download['full_version'], '%core' => $this->core, '%language' => $langcode, '%filename' => '%filename', ); $url = strtr($update_url, $variables); // Download the translation file. Since its contents are volatile, // cache for only 4 hours. if ($filename = _make_download_file($url, 3600 * 4)) { // If this is the core project type, download the translation file // and place it in every profile and an additional copy in // modules/system/translations where it can be detected for import // by other non-default install profiles. if ($this->type === 'core') { $profiles = drush_scan_directory($project_directory . '/profiles', '/.*/', array(), 0, FALSE, 'filename', 0, TRUE); foreach ($profiles as $profile) { if (is_dir($project_directory . '/profiles/' . $profile->basename)) { drush_mkdir($project_directory . '/profiles/' . $profile->basename . '/translations'); drush_copy_dir($filename, $project_directory . '/profiles/' . $profile->basename . '/translations/' . $langcode . '.po'); } } drush_mkdir($project_directory . '/modules/system/translations'); drush_copy_dir($filename, $project_directory . '/modules/system/translations/' . $langcode . '.po'); } else { drush_mkdir($project_directory . '/translations'); drush_copy_dir($filename, $project_directory . '/translations/' . $langcode . '.po', FILE_EXISTS_OVERWRITE); } } else { $failed[] = $langcode; } } if (empty($failed)) { drush_log('All translations downloaded for ' . $this->name, LogLevel::OK); } else { drush_log('Unable to download translations for ' . $this->name . ': ' . implode(', ', $failed), LogLevel::WARNING); } } } return TRUE; } /** * Generate the proper path for this project type. * * @param boolean $base * Whether include the base part (tmp dir). Defaults to TRUE. */ protected function generatePath($base = TRUE) { $path = array(); if ($base) { $path[] = make_tmp(); $path[] = '__build__'; } if (!empty($this->contrib_destination)) { $path[] = $this->contrib_destination; } if (!empty($this->subdir)) { $path[] = $this->subdir; } return implode('/', $path); } /** * Return the proper path for dependencies to be placed in. * * @return string * The path that dependencies will be placed in. */ protected function buildPath($directory) { return $this->base_contrib_destination; } /** * Recurse to process additional makefiles that may be found during * processing. */ function recurse($path) { if (!$this->do_recursion || drush_get_option('no-recursion')) { drush_log(dt("Preventing recursive makefile parsing for !project", array("!project" => $this->name)), LogLevel::NOTICE); return TRUE; } $candidates = array( $this->name . '.make.yml', $this->name . '.make', 'drupal-org.make.yml', 'drupal-org.make', ); $makefile = FALSE; foreach ($candidates as $filename) { if (file_exists($this->download_location . '/' . $filename)) { $makefile = $this->download_location . '/' . $filename; break; } } if (!$makefile) { return TRUE; } drush_log(dt("Found makefile: !makefile", array("!makefile" => basename($makefile))), LogLevel::OK); // Save the original state of the 'custom' context. $custom_context = &drush_get_context('custom'); $original_custom_context_values = $custom_context; $info = make_parse_info_file($makefile, TRUE, $this->options); if (!($info = make_validate_info_file($info))) { $result = FALSE; } else { // Inherit the translations specified in the extender makefile. if (!empty($this->translations)) { $info['translations'] = $this->translations; } // Strip out any modules that have already been processed before this. foreach ($this->manifest as $name) { unset($info['projects'][$name]); } $build_path = $this->buildPath($this->name); make_projects(TRUE, trim($build_path, '/'), $info, $this->build_path, $this->download_location); make_libraries(TRUE, trim($build_path, '/'), $info, $this->build_path, $this->download_location); $result = TRUE; } // Restore original 'custom' context so that any // settings changes made are used. $custom_context = $original_custom_context_values; return $result; } } /** * For processing Drupal core projects. */ class DrushMakeProject_Core extends DrushMakeProject { /** * Override constructor for core to adjust project info. */ protected function __construct(&$project) { parent::__construct($project); // subdir and contrib_destination are not allowed for core. $this->subdir = ''; $this->contrib_destination = ''; } /** * Determine the location to download project to. */ function findDownloadLocation() { $this->path = $this->download_location = $this->generatePath(); $this->project_directory = ''; if (is_dir($this->download_location)) { return drush_set_error('MAKE_DIRECTORY_EXISTS', dt('Directory not empty: !directory', array('!directory' => $this->download_location))); } elseif ($this->download['type'] === 'pm') { // pm-download will create the final __build__ directory, so nothing to do // here. } else { drush_mkdir($this->download_location); } return $this->download_location; } } /** * For processing libraries. */ class DrushMakeProject_Library extends DrushMakeProject { /** * Override constructor for libraries to properly set contrib destination. */ protected function __construct(&$project) { parent::__construct($project); // Allow libraries to specify where they should live in the build path. if (isset($project['destination'])) { $project_path = $project['destination']; } else { $project_path = 'libraries'; } $this->contrib_destination = ($this->base_contrib_destination != '.' ? $this->base_contrib_destination . '/' : '') . $project_path; } /** * No recursion for libraries, sorry :-( */ function recurse($path) { // Return TRUE so that processing continues in the make() method. return TRUE; } /** * No translations for libraries. */ function getTranslations($download_location) { // Return TRUE so that processing continues in the make() method. return TRUE; } } /** * For processing modules. */ class DrushMakeProject_Module extends DrushMakeProject { /** * Override constructor for modules to properly set contrib destination. */ protected function __construct(&$project) { parent::__construct($project); $this->contrib_destination = ($this->base_contrib_destination != '.' ? $this->base_contrib_destination . '/' : '') . 'modules'; } } /** * For processing installation profiles. */ class DrushMakeProject_Profile extends DrushMakeProject { /** * Override contructor for installation profiles to properly set contrib * destination. */ protected function __construct(&$project) { parent::__construct($project); $this->contrib_destination = (!empty($this->destination) ? $this->destination : 'profiles'); } /** * Find the build path. */ protected function buildPath($directory) { return $this->generatePath(FALSE) . '/' . $directory; } } /** * For processing themes. */ class DrushMakeProject_Theme extends DrushMakeProject { /** * Override contructor for themes to properly set contrib destination. */ protected function __construct(&$project) { parent::__construct($project); $this->contrib_destination = ($this->base_contrib_destination != '.' ? $this->base_contrib_destination . '/' : '') . 'themes'; } } /** * For processing translations. */ class DrushMakeProject_Translation extends DrushMakeProject { /** * Override constructor for translations to properly set contrib destination. */ protected function __construct(&$project) { parent::__construct($project); switch ($project['core']) { case '5.x': // Don't think there's an automatic place we can put 5.x translations, // so we'll toss them in a translations directory in the Drupal root. $this->contrib_destination = ($this->base_contrib_destination != '.' ? $this->base_contrib_destination . '/' : '') . 'translations'; break; default: $this->contrib_destination = ''; break; } } } array_filter(drush_get_option_list('projects')), 'libraries' => array_filter(drush_get_option_list('libraries')), ); $info = make_prune_info_file($info, $include_only); if ($info === FALSE || ($info = make_validate_info_file($info)) === FALSE) { return FALSE; } return $info; } /** * Parse makefile recursively. */ function _make_parse_info_file($makefile, $element = 'includes') { if (!($data = make_get_data($makefile))) { return drush_set_error('MAKE_INVALID_MAKE_FILE', dt('Invalid or empty make file: !makefile', array('!makefile' => $makefile))); } // $info['format'] will specify the determined format. $info = _make_determine_format($data); // Set any allowed options. if (!empty($info['options'])) { foreach ($info['options'] as $key => $value) { if (_make_is_override_allowed($key)) { // n.b. 'custom' context has lower priority than 'cli', so // options entered on the command line will "mask" makefile options. drush_set_option($key, $value, 'custom'); } } } // Include any makefiles specified on the command line. if ($include_makefiles = drush_get_option_list('includes', FALSE)) { drush_unset_option('includes'); // Avoid infinite loop. $info['includes'] = is_array($info['includes']) ? $info['includes'] : array(); foreach ($include_makefiles as $include_make) { if (!array_search($include_make, $info['includes'])) { $info['includes'][] = $include_make; } } } // Override elements with values from makefiles specified on the command line. if ($overrides = drush_get_option_list('overrides', FALSE)) { drush_unset_option('overrides'); // Avoid infinite loop. $info['overrides'] = is_array($info['overrides']) ? $info['overrides'] : array(); foreach ($overrides as $override) { if (!array_search($override, $info['overrides'])) { $info['overrides'][] = $override; } } } $info = _make_merge_includes_recursively($info, $makefile); $info = _make_merge_includes_recursively($info, $makefile, 'overrides'); return $info; } /** * Helper function to merge includes recursively. */ function _make_merge_includes_recursively($info, $makefile, $element = 'includes') { if (!empty($info[$element])) { if (is_array($info[$element])) { $includes = array(); foreach ($info[$element] as $key => $include) { if (!empty($include)) { if (!$include_makefile = _make_get_include_path($include, $makefile)) { return make_error('BUILD_ERROR', dt("Cannot determine include file location: !include", array('!include' => $include))); } if ($element == 'overrides') { $info = array_replace_recursive($info, _make_parse_info_file($include_makefile, $element)); } else { $info = array_replace_recursive(_make_parse_info_file($include_makefile), $info); } unset($info[$element][$key]); // Move core back to the top of the list, where // make_generate_from_makefile() expects it. if (!empty($info['projects'])) { array_reverse($info['projects']); } } } } } // Ensure $info['projects'] is an associative array, so that we can merge // includes properly. make_normalize_info($info); return $info; } /** * Helper function to determine the proper path for an include makefile. */ function _make_get_include_path($include, $makefile) { if (is_array($include) && $include['download']['type'] = 'git') { $tmp_dir = make_tmp(); make_download_git($include['makefile'], $include['download']['type'], $include['download'], $tmp_dir); $include_makefile = $tmp_dir . '/' . $include['makefile']; } elseif (is_string($include)) { $include_path = dirname($makefile); if (make_valid_url($include, TRUE)) { $include_makefile = $include; } elseif (file_exists($include_path . '/' . $include)) { $include_makefile = $include_path . '/' . $include; } elseif (file_exists($include)) { $include_makefile = $include; } else { return make_error('BUILD_ERROR', dt("Include file missing: !include", array('!include' => $include))); } } else { return FALSE; } return $include_makefile; } /** * Expand shorthand elements, so that we have an associative array. */ function make_normalize_info(&$info) { if (isset($info['projects'])) { foreach($info['projects'] as $key => $project) { if (is_numeric($key) && is_string($project)) { unset($info['projects'][$key]); $info['projects'][$project] = array( 'version' => '', ); } if (is_string($key) && is_numeric($project)) { $info['projects'][$key] = array( 'version' => $project, ); } } } } /** * Remove entries in the info file in accordance with the options passed in. * Entries are either explicitly 'allowed' (with the $include_only parameter) in * which case all *other* entries will be excluded. * * @param array $info * A parsed info file. * * @param array $include_only * (Optional) Array keyed by entry type (e.g. 'libraries') against an array of * allowed keys for that type. The special value '*' means 'all entries of * this type'. If this parameter is omitted, no entries will be excluded. * * @return array * The $info array, pruned if necessary. */ function make_prune_info_file($info, $include_only = array()) { // We may get passed FALSE in some cases. // Also we cannot prune an empty array, so no point in this code running! if (empty($info)) { return $info; } // We will accrue an explanation of our activities here. $msg = array(); $msg['scope'] = dt("Drush make restricted to the following entries:"); $pruned = FALSE; if (count(array_filter($include_only))) { $pruned = TRUE; foreach ($include_only as $type => $keys) { if (!isset($info[$type])) { continue; } // For translating // dt("Projects"); // dt("Libraries"); $type_title = dt(ucfirst($type)); // Handle the special '*' value. if (in_array('*', $keys)) { $msg[$type] = dt("!entry_type: ", array('!entry_type' => $type_title)); } // Handle a (possibly empty) array of keys to include/exclude. else { $info[$type] = array_intersect_key($info[$type], array_fill_keys($keys, 1)); unset($msg[$type]); if (!empty($info[$type])) { $msg[$type] = dt("!entry_type: !make_entries", array('!entry_type' => $type_title, '!make_entries' => implode(', ', array_keys($info[$type])))); } } } } if ($pruned) { // Make it clear to the user what's going on. drush_log(implode("\n", $msg), LogLevel::OK); // Throw an error if these restrictions reduced the make to nothing. if (empty($info['projects']) && empty($info['libraries'])) { // This error mentions the options explicitly to make it as clear as // possible to the user why this error has occurred. make_error('BUILD_ERROR', dt("All projects and libraries have been excluded. Review the 'projects' and 'libraries' options.")); } } return $info; } /** * Validate the make file. */ function make_validate_info_file($info) { // Assume no errors to start. $errors = FALSE; if (empty($info['core'])) { make_error('BUILD_ERROR', dt("The 'core' attribute is required")); $errors = TRUE; } // Standardize on core. elseif (preg_match('/^(\d+)(\.(x|(\d+)(-[a-z0-9]+)?))?$/', $info['core'], $matches)) { // An exact version of core has been specified, so pass that to an // internal variable for storage. if (isset($matches[4])) { $info['core_release'] = $info['core']; } // Format the core attribute consistently. $info['core'] = $matches[1] . '.x'; } else { make_error('BUILD_ERROR', dt("The 'core' attribute !core has an incorrect format.", array('!core' => $info['core']))); $errors = TRUE; } if (!isset($info['api'])) { $info['api'] = MAKE_API; drush_log(dt("You need to specify an API version of two in your makefile:\napi = !api", array("!api" => MAKE_API)), LogLevel::WARNING); } elseif ($info['api'] != MAKE_API) { make_error('BUILD_ERROR', dt("The specified API attribute is incompatible with this version of Drush Make.")); $errors = TRUE; } $names = array(); // Process projects. if (isset($info['projects'])) { if (!is_array($info['projects'])) { make_error('BUILD_ERROR', dt("'projects' attribute must be an array.")); $errors = TRUE; } else { // Filter out entries that have been forcibly removed via [foo] = FALSE. $info['projects'] = array_filter($info['projects']); foreach ($info['projects'] as $project => $project_data) { // Project has an attributes array. if (is_string($project) && is_array($project_data)) { if (in_array($project, $names)) { make_error('BUILD_ERROR', dt("Project !project defined twice (remove the first projects[] = !project).", array('!project' => $project))); $errors = TRUE; } $names[] = $project; foreach ($project_data as $attribute => $value) { // Prevent malicious attempts to access other areas of the // filesystem. if (in_array($attribute, array('subdir', 'directory_name', 'contrib_destination')) && !make_safe_path($value)) { $args = array( '!path' => $value, '!attribute' => $attribute, '!project' => $project, ); make_error('BUILD_ERROR', dt("Illegal path !path for '!attribute' attribute in project !project.", $args)); $errors = TRUE; } } } // Cover if there is no project info, it's just a project name. elseif (is_numeric($project) && is_string($project_data)) { if (in_array($project_data, $names)) { make_error('BUILD_ERROR', dt("Project !project defined twice (remove the first projects[] = !project).", array('!project' => $project_data))); $errors = TRUE; } $names[] = $project_data; unset($info['projects'][$project]); $info['projects'][$project_data] = array(); } // Convert shorthand project version style to array format. elseif (is_string($project_data)) { if (in_array($project, $names)) { make_error('BUILD_ERROR', dt("Project !project defined twice (remove the first projects[] = !project).", array('!project' => $project))); $errors = TRUE; } $names[] = $project; $info['projects'][$project] = array('version' => $project_data); } else { make_error('BUILD_ERROR', dt('Project !project incorrectly specified.', array('!project' => $project))); $errors = TRUE; } } } } if (isset($info['libraries'])) { if (!is_array($info['libraries'])) { make_error('BUILD_ERROR', dt("'libraries' attribute must be an array.")); $errors = TRUE; } else { // Filter out entries that have been forcibly removed via [foo] = FALSE. $info['libraries'] = array_filter($info['libraries']); foreach ($info['libraries'] as $library => $library_data) { if (is_array($library_data)) { foreach ($library_data as $attribute => $value) { // Unset disallowed attributes. if (in_array($attribute, array('contrib_destination'))) { unset($info['libraries'][$library][$attribute]); } // Prevent malicious attempts to access other areas of the // filesystem. elseif (in_array($attribute, array('contrib_destination', 'directory_name')) && !make_safe_path($value)) { $args = array( '!path' => $value, '!attribute' => $attribute, '!library' => $library, ); make_error('BUILD_ERROR', dt("Illegal path !path for '!attribute' attribute in library !library.", $args)); $errors = TRUE; } } } } } } // Convert shorthand project/library download style to array format. foreach (array('projects', 'libraries') as $type) { if (isset($info[$type]) && is_array($info[$type])) { foreach ($info[$type] as $name => $item) { if (!empty($item['download']) && is_string($item['download'])) { $info[$type][$name]['download'] = array('url' => $item['download']); } } } } // Apply defaults after projects[] array has been expanded, but prior to // external validation. make_apply_defaults($info); foreach (drush_command_implements('make_validate_info') as $module) { $function = $module . '_make_validate_info'; $return = $function($info); if ($return) { $info = $return; } else { $errors = TRUE; } } if ($errors) { return FALSE; } return $info; } /** * Verify the syntax of the given URL. * * Copied verbatim from includes/common.inc * * @see valid_url */ function make_valid_url($url, $absolute = FALSE) { if ($absolute) { return (bool) preg_match(" /^ # Start at the beginning of the text (?:ftp|https?):\/\/ # Look for ftp, http, or https schemes (?: # Userinfo (optional) which is typically (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)* # a username or a username and password (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@ # combination )? (?: (?:[a-z0-9\-\.]|%[0-9a-f]{2})+ # A domain name or a IPv4 address |(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\]) # or a well formed IPv6 address ) (?::[0-9]+)? # Server port number (optional) (?:[\/|\?] (?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2}) # The path and query (optional) *)? $/xi", $url); } else { return (bool) preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url); } } /** * Find, and possibly create, a temporary directory. * * @param boolean $set * Must be TRUE to create a directory. * @param string $directory * Pass in a directory to use. This is required if using any * concurrent operations. * * @todo Merge with drush_tempdir(). */ function make_tmp($set = TRUE, $directory = NULL) { static $tmp_dir; if (isset($directory) && !isset($tmp_dir)) { $tmp_dir = $directory; } if (!isset($tmp_dir) && $set) { $tmp_dir = drush_find_tmp(); if (strrpos($tmp_dir, '/') == strlen($tmp_dir) - 1) { $tmp_dir .= 'make_tmp_' . time() . '_' . uniqid(); } else { $tmp_dir .= '/make_tmp_' . time() . '_' . uniqid(); } if (!drush_get_option('no-clean', FALSE)) { drush_register_file_for_deletion($tmp_dir); } if (file_exists($tmp_dir)) { return make_tmp(TRUE); } // Create the directory. drush_mkdir($tmp_dir); } return $tmp_dir; } /** * Removes the temporary build directory. On failed builds, this is handled by * drush_register_file_for_deletion(). */ function make_clean_tmp() { if (!($tmp_dir = make_tmp(FALSE))) { return; } if (!drush_get_option('no-clean', FALSE)) { drush_delete_dir($tmp_dir); } else { drush_log(dt('Temporary directory: !dir', array('!dir' => $tmp_dir)), LogLevel::OK); } } /** * Prepare a Drupal installation, copying default.settings.php to settings.php. */ function make_prepare_install($build_path) { $default = make_tmp() . '/__build__/sites/default'; drush_copy_dir($default . DIRECTORY_SEPARATOR . 'default.settings.php', $default . DIRECTORY_SEPARATOR . 'settings.php', FILE_EXISTS_OVERWRITE); drush_mkdir($default . '/files'); chmod($default . DIRECTORY_SEPARATOR . 'settings.php', 0666); chmod($default . DIRECTORY_SEPARATOR . 'files', 0777); } /** * Calculate a cksum on each file in the build, and md5 the resulting hashes. */ function make_md5() { return drush_dir_md5(make_tmp()); } /** * @todo drush_archive_dump() also makes a tar. Consolidate? */ function make_tar($build_path) { $tmp_path = make_tmp(); drush_mkdir(dirname($build_path)); $filename = basename($build_path); $dirname = basename($build_path, '.tar.gz'); // Move the build directory to a more human-friendly name, so that tar will // use it instead. drush_move_dir($tmp_path . DIRECTORY_SEPARATOR . '__build__', $tmp_path . DIRECTORY_SEPARATOR . $dirname, TRUE); // Only move the tar file to it's final location if it's been built // successfully. if (drush_shell_exec("%s -C %s -Pczf %s %s", drush_get_tar_executable(), $tmp_path, $tmp_path . '/' . $filename, $dirname)) { drush_move_dir($tmp_path . DIRECTORY_SEPARATOR . $filename, $build_path, TRUE); }; // Move the build directory back to it's original location for consistency. drush_move_dir($tmp_path . DIRECTORY_SEPARATOR . $dirname, $tmp_path . DIRECTORY_SEPARATOR . '__build__'); } /** * Logs an error unless the --force-complete command line option is specified. */ function make_error($error_code, $message) { if (drush_get_option('force-complete')) { drush_log("$error_code: $message -- build forced", LogLevel::WARNING); } else { return drush_set_error($error_code, $message); } } /** * Checks an attribute's path to ensure it's not maliciously crafted. * * @param string $path * The path to check. */ function make_safe_path($path) { return !preg_match("+^/|^\.\.|/\.\./+", $path); } /** * Get data based on the source. * * This is a helper function to abstract the retrieval of data, so that it can * come from files, STDIN, etc. Currently supports filepath and STDIN. * * @param string $data_source * The path to a file, or '-' for STDIN. * * @return string * The raw data as a string. */ function make_get_data($data_source) { if ($data_source == '-') { // See http://drupal.org/node/499758 before changing this. $stdin = fopen('php://stdin', 'r'); $data = ''; $has_input = FALSE; while ($line = fgets($stdin)) { $has_input = TRUE; $data .= $line; } if ($has_input) { return $data; } return FALSE; } // Local file. elseif (!strpos($data_source, '://')) { $data = file_get_contents($data_source); } // Remote file. else { $file = _make_download_file($data_source); $data = file_get_contents($file); drush_op('unlink', $file); } return $data; } /** * Apply any defaults. * * @param array &$info * A parsed make array. */ function make_apply_defaults(&$info) { if (isset($info['defaults'])) { $defaults = $info['defaults']; foreach ($defaults as $type => $default_data) { if (isset($info[$type])) { foreach ($info[$type] as $project => $data) { $info[$type][$project] = _drush_array_overlay_recursive($default_data, $info[$type][$project]); } } else { drush_log(dt("Unknown attribute '@type' in defaults array", array('@type' => $type)), LogLevel::WARNING); } } } } /** * Check if makefile overrides are allowed * * @param array $option * The option to check. */ function _make_is_override_allowed ($option) { $allow_override = drush_get_option('allow-override', 'all'); if ($allow_override == 'all') { $allow_override = array(); } elseif (!is_array($allow_override)) { $allow_override = _convert_csv_to_array($allow_override); } if ((empty($allow_override)) || ((in_array($option, $allow_override)) && (!in_array('none', $allow_override)))) { return TRUE; } drush_log(dt("'!option' not allowed; use --allow-override=!option or --allow-override=all to permit", array("!option" => $option)), LogLevel::WARNING); return FALSE; } /** * Gather any working copy options. * * @param array $download * The download array. */ function _get_working_copy_option($download) { $wc = ''; if (_make_is_override_allowed('working-copy') && isset ($download['working-copy'])) { $wc = $download['working-copy']; } else { $wc = drush_get_option('working-copy'); } return $wc; } /** * Given data from stdin, determine format. * * @return array|bool * Returns parsed data if it matches any known format. */ function _make_determine_format($data) { // Most .make files will have a `core` attribute. Use this to determine // the format. if (preg_match('/^\s*core:/m', $data)) { $parsed = ParserYaml::parse($data); $parsed['format'] = 'yaml'; return $parsed; } elseif (preg_match('/^\s*core\s*=/m', $data)) { $parsed = ParserIni::parse($data); $parsed['format'] = 'ini'; return $parsed; } // If the .make file did not have a core attribute, it is being included // by another .make file. Test YAML first to avoid segmentation faults from // preg_match in INI parser. $yaml_parse_exception = FALSE; try { if ($parsed = ParserYaml::parse($data)) { $parsed['format'] = 'yaml'; return $parsed; } } catch (\Symfony\Component\Yaml\Exception\ParseException $e) { // Note that an exception was thrown, and display after .ini parsing. $yaml_parse_exception = $e; } // Try INI format. if ($parsed = ParserIni::parse($data)) { $parsed['format'] = 'ini'; return $parsed; } if ($yaml_parse_exception) { throw $e; } return drush_set_error('MAKE_STDIN_ERROR', dt('Unknown make file format')); } $project) { if (($project['download']['type'] == 'git') && !empty($project['download']['url'])) { // TODO check that tag or branch are valid version strings (with pm_parse_version()). if (!empty($project['download']['tag'])) { $version = $project['download']['tag']; } elseif (!empty($project['download']['branch'])) { $version = $project['download']['branch'] . '-dev'; } /* elseif (!empty($project['download']['refspec'])) { #TODO# Parse refspec. } */ else { // If no tag or branch, we can't match a d.o version. continue; } $projects[$project_name] = $project + array( 'path' => '', 'label' => $project_name, 'version' => $version, ); } elseif ($project['download']['type'] == 'pm') { $projects[$project_name] = $project + array( 'path' => '', 'label' => $project_name, ); } } // Check for updates. $update_status = drush_get_engine('update_status'); $update_info = $update_status->getStatus($projects, TRUE); $security_only = drush_get_option('security-only', FALSE); foreach ($update_info as $project_name => $project_update_info) { if (!$security_only || ($security_only && $project_update_info['status'] == DRUSH_UPDATESTATUS_NOT_SECURE)) { $make_projects[$project_name]['download']['full_version'] = $project_update_info['recommended']; } } // Inject back make projects and generate the updated makefile. drush_set_option('DRUSH_MAKE_PROJECTS', $make_projects); make_generate_from_makefile(drush_get_option('result-file'), $makefile); } $destination))); if (!drush_get_context('DRUSH_SIMULATE')) { if (drush_confirm(dt('Would you like to create it?'))) { drush_mkdir($destination, TRUE); } if (!is_dir($destination)) { return drush_set_error('DRUSH_PM_NO_DESTINATION', dt('Unable to create destination directory !destination.', array('!destination' => $destination))); } } } if (!is_writable($destination)) { return drush_set_error('DRUSH_PM_NO_DESTINATION', dt('Destination directory !destination is not writable.', array('!destination' => $destination))); } // Ignore --use-site-dir, if given. if (drush_get_option('use-site-dir', FALSE)) { drush_set_option('use-site-dir', FALSE); } } // Validate --variant or enforce a sane default. $variant = drush_get_option('variant', FALSE); if ($variant) { $variants = array('full', 'projects', 'profile-only'); if (!in_array($variant, $variants)) { return drush_set_error('DRUSH_PM_PROFILE_INVALID_VARIANT', dt('Invalid variant !variant. Valid values: !variants.', array('!variant' => $variant, '!variants' => implode(', ', $variants)))); } } // 'full' and 'projects' variants are only valid for wget package handler. $package_handler = drush_get_option('package-handler', 'wget'); if (($package_handler != 'wget') && ($variant != 'profile-only')) { $new_variant = 'profile-only'; if ($variant) { drush_log(dt('Variant !variant is incompatible with !ph package-handler.', array('!variant' => $variant, '!ph' => $package_handler)), LogLevel::WARNING); } } // If we are working on a drupal root, full variant is not an option. else if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_ROOT) { if ((!$variant) || (($variant == 'full') && (!isset($new_variant)))) { $new_variant = 'projects'; } if ($variant == 'full') { drush_log(dt('Variant full is not a valid option within a Drupal root.'), LogLevel::WARNING); } } if (isset($new_variant)) { drush_set_option('variant', $new_variant); if ($variant) { drush_log(dt('Switching to --variant=!variant.', array('!variant' => $new_variant)), LogLevel::OK); } } } /** * Command callback. Download Drupal core or any project. */ function drush_pm_download() { $release_info = drush_get_engine('release_info'); if (!$requests = pm_parse_arguments(func_get_args(), FALSE)) { $requests = array('drupal'); } // Pick cli options. $status_url = drush_get_option('source', ReleaseInfo::DEFAULT_URL); $restrict_to = drush_get_option('dev', ''); $select = drush_get_option('select', 'auto'); $all = drush_get_option('all', FALSE); // If we've bootstrapped a Drupal site and the user may have the chance // to select from a list of filtered releases, we want to pass // the installed project version, if any. $projects = array(); if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) { if (!$all and in_array($select, array('auto', 'always'))) { $projects = drush_get_projects(); } } // Get release history for each request and download the project. foreach ($requests as $request) { $request = pm_parse_request($request, $status_url, $projects); $version = isset($projects[$request['name']]) ? $projects[$request['name']]['version'] : NULL; $release = $release_info->selectReleaseBasedOnStrategy($request, $restrict_to, $select, $all, $version); if ($release == FALSE) { // Stop working on the first failure. Return silently on user abort. if (drush_get_context('DRUSH_USER_ABORT', FALSE)) { return FALSE; } // Signal that the command failed for all other problems. return drush_set_error('DRUSH_DOWNLOAD_FAILED', dt("Could not download requested project(s).")); } $request['version'] = $release['version']; $project_release_info = $release_info->get($request); $request['project_type'] = $project_release_info->getType(); // Determine the name of the directory that will contain the project. // We face here all the assymetries to make it smooth for package handlers. // For Drupal core: --drupal-project-rename or drupal-x.y if (($request['project_type'] == 'core') || (($request['project_type'] == 'profile') && (drush_get_option('variant', 'full') == 'full'))) { // Avoid downloading core into existing core. if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_ROOT) { if (strpos(realpath(drush_get_option('destination')), DRUPAL_ROOT) !== FALSE) { return drush_set_error('DRUSH_PM_DOWNLOAD_TRANSLATIONS_FORBIDDEN', dt('It\'s forbidden to download !project core into an existing core.', array('!project' => $request['name']))); } } if ($rename = drush_get_option('drupal-project-rename', FALSE)) { if ($rename === TRUE) { $request['project_dir'] = $request['name']; } else { $request['project_dir'] = $rename; } } else { // Set to drupal-x.y, the expected name for .tar.gz contents. // Explicitly needed for cvs package handler. $request['project_dir'] = strtolower(strtr($release['name'], ' ', '-')); } } // For the other project types we want the project name. Including core // variant for profiles. Note those come with drupal-x.y in the .tar.gz. else { $request['project_dir'] = $request['name']; } // Download the project to a temporary location. drush_log(dt('Downloading project !name ...', array('!name' => $request['name']))); $request['full_project_path'] = package_handler_download_project($request, $release); if (!$request['full_project_path']) { // Delete the cached update service file since it may be invalid. $release_info->clearCached($request); drush_log(dt('Error downloading !name', array('!name' => $request['name']), LogLevel::ERROR)); continue; } // Determine the install location for the project. User provided // --destination has preference. $destination = drush_get_option('destination'); if (!empty($destination)) { if (!file_exists($destination)) { drush_mkdir($destination); } $request['project_install_location'] = realpath($destination); } else { $request['project_install_location'] = _pm_download_destination($request['project_type']); } // If user did not provide --destination, then call the // download-destination-alter hook to give the chance to any commandfiles // to adjust the install location or abort it. if (empty($destination)) { $result = drush_command_invoke_all_ref('drush_pm_download_destination_alter', $request, $release); if (array_search(FALSE, $result, TRUE) !== FALSE) { return FALSE; } } // Load version control engine and detect if (the parent directory of) the // project install location is under a vcs. if (!$version_control = drush_pm_include_version_control($request['project_install_location'])) { continue; } $request['project_install_location'] .= '/' . $request['project_dir']; if ($version_control->engine == 'backup') { // Check if install location already exists. if (is_dir($request['project_install_location'])) { if (drush_confirm(dt('Install location !location already exists. Do you want to overwrite it?', array('!location' => $request['project_install_location'])))) { drush_delete_dir($request['project_install_location'], TRUE); } else { drush_log(dt("Skip installation of !project to !dest.", array('!project' => $request['name'], '!dest' => $request['project_install_location'])), LogLevel::WARNING); continue; } } } else { // Find and unlink all files but the ones in the vcs control directories. $skip_list = array('.', '..'); $skip_list = array_merge($skip_list, drush_version_control_reserved_files()); drush_scan_directory($request['project_install_location'], '/.*/', $skip_list, 'unlink', TRUE, 'filename', 0, TRUE); } // Copy the project to the install location. if (drush_op('_drush_recursive_copy', $request['full_project_path'], $request['project_install_location'])) { drush_log(dt("Project !project (!version) downloaded to !dest.", array('!project' => $request['name'], '!version' => $release['version'], '!dest' => $request['project_install_location'])), LogLevel::SUCCESS); // Adjust full_project_path to the final project location. $request['full_project_path'] = $request['project_install_location']; // If the version control engine is a proper vcs we also need to remove // orphan directories. if ($version_control->engine != 'backup') { $empty_dirs = drush_find_empty_directories($request['full_project_path'], $version_control->reserved_files()); foreach ($empty_dirs as $empty_dir) { // Some VCS files are read-only on Windows (e.g., .svn/entries). drush_delete_dir($empty_dir, TRUE); } } // Post download actions. package_handler_post_download($request, $release); drush_command_invoke_all('drush_pm_post_download', $request, $release); $version_control->post_download($request); // Print release notes if --notes option is set. if (drush_get_option('notes') && !drush_get_context('DRUSH_PIPE')) { $project_release_info->getReleaseNotes($release['version'], FALSE); } // Inform the user about available modules a/o themes in the downloaded project. drush_pm_extensions_in_project($request); } else { // We don't `return` here in order to proceed with downloading additional projects. drush_set_error('DRUSH_PM_DOWNLOAD_FAILED', dt("Project !project (!version) could not be downloaded to !dest.", array('!project' => $request['name'], '!version' => $release['version'], '!dest' => $request['project_install_location']))); } // Notify about this project. if (drush_notify_allowed('pm-download')) { $msg = dt('Project !project (!version) downloaded to !install.', array( '!project' => $name, '!version' => $release['version'], '!install' => $request['project_install_location'], )); drush_notify_send(drush_notify_command_message('pm-download', $msg)); } } } /** * Implementation of hook_drush_pm_download_destination_alter(). * * Built-in download-destination-alter hook. This particular version of * the hook will move modules that contain only Drush commands to * /usr/share/drush/commands if it exists, or $HOME/.drush if the * site-wide location does not exist. */ function pm_drush_pm_download_destination_alter(&$request, $release) { // A module is a pure Drush command if it has no .info.yml (8+) and contains no // .drush.inc files. Skip this test for Drush itself, though; we do // not want to download Drush to the ~/.drush folder. if (in_array($request['project_type'], array('module', 'utility')) && ($request['name'] != 'drush')) { $drush_command_files = drush_scan_directory($request['full_project_path'], '/.*\.drush.inc/'); if (!empty($drush_command_files)) { $pattern = drush_drupal_major_version() >= 8 ? '/.*\.info/' : '/.*\.module/'; $module_files = drush_scan_directory($request['full_project_path'], $pattern); if (empty($module_files)) { $install_dir = drush_get_context('DRUSH_SITE_WIDE_COMMANDFILES'); if (!is_dir($install_dir) || !is_writable($install_dir)) { $install_dir = drush_get_context('DRUSH_PER_USER_CONFIGURATION'); } // Make the .drush dir if it does not already exist. if (!is_dir($install_dir)) { drush_mkdir($install_dir, FALSE); } // Change the location if the mkdir worked. if (is_dir($install_dir)) { $request['project_install_location'] = $install_dir; } } // We need to clear the Drush commandfile cache so that // our newly-downloaded Drush extension commandfiles can be found. drush_cache_clear_all(); } } } /** * Determines a candidate destination directory for a particular site path. * * Optionally attempts to create the directory. * * @return String the candidate destination if it exists. */ function _pm_download_destination_lookup($type, $drupal_root, $sitepath, $create = FALSE) { // Profiles in Drupal < 8 if (($type == 'profile') && (drush_drupal_major_version() < 8)) { $destination = 'profiles'; } // Type: module, theme or profile. else { if ($type == 'theme engine') { $destination = 'themes/engines'; } else { $destination = $type . 's'; } // Prefer /contrib if it exists. if ($sitepath) { $destination = $sitepath . '/' . $destination; } $contrib = $destination . '/contrib'; if (is_dir($contrib)) { $destination = $contrib; } } if ($create) { drush_log(dt('Attempting to create destination directory at !dir', array('!dir' => $destination))); drush_mkdir($destination, TRUE); } if (is_dir($destination)) { drush_log(dt('Using destination directory !dir', array('!dir' => $destination))); return $destination; } drush_log(dt('Could not find destination directory at !dir', array('!dir' => $destination))); return FALSE; } /** * Returns the best destination for a particular download type we can find. * * It is based on the project type and drupal and site contexts. */ function _pm_download_destination($type) { $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT'); $site_root = drush_get_context('DRUSH_DRUPAL_SITE_ROOT'); $full_site_root = (empty($drupal_root) || empty($site_root)) ? '' : $drupal_root .'/'. $site_root; $sitewide = empty($drupal_root) ? '' : $drupal_root . '/' . drush_drupal_sitewide_directory(); $in_site_directory = FALSE; // Check if we are running within the site directory. if (strpos(realpath(drush_cwd()), realpath($full_site_root)) !== FALSE || (drush_get_option('use-site-dir', FALSE))) { $in_site_directory = TRUE; } $destination = ''; if ($type != 'core') { // Attempt 1: If we are in a specific site directory, and the destination // directory already exists, then we use that. if (empty($destination) && $site_root && $in_site_directory) { $create_dir = drush_get_option('use-site-dir', FALSE); $destination = _pm_download_destination_lookup($type, $drupal_root, $full_site_root, $create_dir); } // Attempt 2: If the destination directory already exists for // the sitewide directory, use that. if (empty($destination) && $drupal_root) { $destination = _pm_download_destination_lookup($type, $drupal_root, $sitewide); } // Attempt 3: If a specific (non default) site directory exists and // the sitewide directory does not exist, then create destination // in the site specific directory. if (empty($destination) && $site_root && $site_root !== 'sites/default' && is_dir($full_site_root) && !is_dir($sitewide)) { $destination = _pm_download_destination_lookup($type, $drupal_root, $full_site_root, TRUE); } // Attempt 4: If sitewide directory exists, then create destination there. if (empty($destination) && is_dir($sitewide)) { $destination = _pm_download_destination_lookup($type, $drupal_root, $sitewide, TRUE); } // Attempt 5: If site directory exists (even default), then create // destination in that directory. if (empty($destination) && $site_root && is_dir($full_site_root)) { $destination = _pm_download_destination_lookup($type, $drupal_root, $full_site_root, TRUE); } } // Attempt 6: If we didn't find a valid directory yet (or we somehow found // one that doesn't exist) we always fall back to the current directory. if (empty($destination) || !is_dir($destination)) { $destination = drush_cwd(); } return $destination; } $extension)), LogLevel::WARNING); continue; } if (drush_extension_get_type($info) == 'module') { $data = _drush_pm_info_module($info); } else { $data = _drush_pm_info_theme($info); } $result[$extension] = $data; } return $result; } /** * Output format formatter-filter callback. * * @see drush_parse_command() * @see drush_outputformat */ function _drush_pm_info_format_table_data($data) { $result = array(); foreach ($data as $extension => $info) { foreach($info as $key => $value) { if (is_array($value)) { if (empty($value)) { $value = 'none'; } else { $value = implode(', ', $value); } } $result[$extension][$key] = $value; } } return $result; } /** * Return an array with general info of an extension. */ function _drush_pm_info_extension($info) { $data['extension'] = drush_extension_get_name($info); $data['project'] = isset($info->info['project'])?$info->info['project']:dt('Unknown'); $data['type'] = drush_extension_get_type($info); $data['title'] = $info->info['name']; $data['config'] = isset($info->info['configure']) ? $info->info['configure'] : dt('None'); $data['description'] = $info->info['description']; $data['version'] = $info->info['version']; $data['date'] = isset($info->info['datestamp']) ? format_date($info->info['datestamp'], 'custom', 'Y-m-d') : NULL; $data['package'] = $info->info['package']; $data['core'] = $info->info['core']; $data['php'] = $info->info['php']; $data['status'] = drush_get_extension_status($info); $data['path'] = drush_extension_get_path($info); return $data; } /** * Return an array with info of a module. */ function _drush_pm_info_module($info) { $major_version = drush_drupal_major_version(); $data = _drush_pm_info_extension($info); if ($info->schema_version > 0) { $schema_version = $info->schema_version; } elseif ($info->schema_version == -1) { $schema_version = "no schema installed"; } else { $schema_version = "module has no schema"; } $data['schema_version'] = $schema_version; if ($major_version == 7) { $data['files'] = $info->info['files']; } $data['requires'] = $info->info['dependencies']; if ($major_version == 6) { $requiredby = $info->info['dependents']; } else { $requiredby = array_keys($info->required_by); } $data['required_by'] = $requiredby; if ($info->status == 1) { $role = drush_role_get_class(); $data['permissions'] = $role->getModulePerms(drush_extension_get_name($info)); } return $data; } /** * Return an array with info of a theme. */ function _drush_pm_info_theme($info) { $major_version = drush_drupal_major_version(); $data = _drush_pm_info_extension($info); $data['core'] = $info->info['core']; $data['php'] = $info->info['php']; $data['engine'] = $info->info['engine']; $data['base_theme'] = isset($info->base_themes) ? implode($info->base_themes, ', ') : ''; $regions = $info->info['regions']; $data['regions'] = $regions; $features = $info->info['features']; $data['features'] = $features; if (count($info->info['stylesheets']) > 0) { $data['stylesheets'] = ''; foreach ($info->info['stylesheets'] as $media => $files) { $files = array_keys($files); $data['media '.$media] = $files; } } if (count($info->info['scripts']) > 0) { $scripts = array_keys($info->info['scripts']); $data['scripts'] = $scripts; } return $data; } =1.7 // (avoid drush_shell_exec because we want to run this even in --simulated mode.) $success = exec('git --version', $git); $git_version_array = explode(" ", $git[0]); $git_version = $git_version_array[2]; drush_set_context('DRUSH_DEBUG', $debug); if (!$success) { return drush_set_error('DRUSH_SHELL_COMMAND_NOT_FOUND', dt('git executable not found.')); } elseif ($git_version < '1.7') { return drush_set_error('GIT_VERSION_UNSUPPORTED', dt('Your git version !git_version is not supported; please upgrade to git 1.7 or later.', array('!git_version' => $git_version))); } // Check git_deploy is enabled. Only for bootstrapped sites. if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) { drush_include_engine('drupal', 'environment'); if (!drush_get_option('gitinfofile') && !drush_module_exists('git_deploy')) { drush_log(dt('git package handler needs git_deploy module enabled to work properly.'), LogLevel::WARNING); } } return TRUE; } /** * Download a project. * * @param $request * The project array with name, base and full (final) paths. * @param $release * The release details array from drupal.org. */ function package_handler_download_project(&$request, $release) { if ($username = drush_get_option('gitusername')) { // Uses SSH, which enables pushing changes back to git.drupal.org. $repository = $username . '@git.drupal.org:project/' . $request['name'] . '.git'; } else { $repository = 'git://git.drupal.org/project/' . $request['name'] . '.git'; } $request['repository'] = $repository; $tag = $release['tag']; // If the --cache option was given, create a new git reference cache of the // remote repository, or update the existing cache to fetch recent changes. if (drush_get_option('cache') && ($cachedir = drush_directory_cache())) { $gitcache = $cachedir . '/git'; $projectcache = $gitcache . '/' . $request['name'] . '.git'; drush_mkdir($gitcache); // Setup a new cache, if we don't have this project yet. if (!file_exists($projectcache)) { // --mirror works similar to --bare, but retrieves all tags, local // branches, remote branches, and any other refs (notes, stashes, etc). // @see http://stackoverflow.com/questions/3959924 $command = 'git clone --mirror'; if (drush_get_context('DRUSH_VERBOSE')) { $command .= ' --verbose --progress'; } $command .= ' %s %s'; drush_shell_cd_and_exec($gitcache, $command, $repository, $request['name'] . '.git'); } // If we already have this project, update it to speed up subsequent clones. else { // A --mirror clone is fully synchronized with `git remote update` instead // of `git fetch --all`. // @see http://stackoverflow.com/questions/6150188 drush_shell_cd_and_exec($projectcache, 'git remote update'); } $gitcache = $projectcache; } // Clone the repo into a temporary path. $clone_path = drush_tempdir(); $command = 'git clone'; $command .= ' ' . drush_get_option('gitcloneparams'); if (drush_get_option('cache')) { $command .= ' --reference ' . drush_escapeshellarg($gitcache); } if (drush_get_context('DRUSH_VERBOSE')) { $command .= ' --verbose --progress'; } $command .= ' ' . drush_escapeshellarg($repository); $command .= ' ' . drush_escapeshellarg($clone_path); if (!drush_shell_exec($command)) { return drush_set_error('DRUSH_PM_GIT_CHECKOUT_PROBLEMS', dt('Unable to clone project !name from git.drupal.org.', array('!name' => $request['name']))); } // Check if the 'tag' from the release feed is a tag or a branch. // If the tag exists, git will return it if (!drush_shell_cd_and_exec($clone_path, 'git tag -l ' . drush_escapeshellarg($tag))) { return drush_set_error('DRUSH_PM_GIT_CHECKOUT_PROBLEMS', dt('Unable to clone project !name from git.drupal.org.', array('!name' => $request['name']))); } $output = drush_shell_exec_output(); if (isset($output[0]) && ($output[0] == $tag)) { // If we want a tag, simply checkout it. The checkout will end up in // "detached head" state. $command = 'git checkout ' . drush_get_option('gitcheckoutparams'); $command .= ' ' . drush_escapeshellarg($tag); if (!drush_shell_cd_and_exec($clone_path, $command)) { return drush_set_error('DRUSH_PM_UNABLE_CHECKOUT', 'Unable to retrieve ' . $request['name'] . ' from git.drupal.org.'); } } else { // Else, we want to checkout a branch. // First check if we are not already in the correct branch. if (!drush_shell_cd_and_exec($clone_path, 'git symbolic-ref HEAD')) { return drush_set_error('DRUSH_PM_UNABLE_CHECKOUT', 'Unable to retrieve ' . $request['name'] . ' from git.drupal.org.'); } $output = drush_shell_exec_output(); $current_branch = preg_replace('@^refs/heads/@', '', $output[0]); // If we are not on the correct branch already, switch to the correct one. if ($current_branch != $tag) { $command = 'git checkout'; $command .= ' ' . drush_get_option('gitcheckoutparams'); $command .= ' --track ' . drush_escapeshellarg('origin/' . $tag) . ' -b ' . drush_escapeshellarg($tag); if (!drush_shell_cd_and_exec($clone_path, $command)) { return drush_set_error('DRUSH_PM_UNABLE_CHECKOUT', 'Unable to retrieve ' . $request['name'] . ' from git.drupal.org.'); } } } return $clone_path; } /** * Update a project (so far, only modules are supported). * * @param $request * The project array with name, base and full (final) paths. * @param $release * The release details array from drupal.org. */ function package_handler_update_project($request, $release) { drush_log('Updating project ' . $request['name'] . ' ...'); $commands = array(); if ((!empty($release['version_extra'])) && ($release['version_extra'] == 'dev')) { // Update the branch of the development repository. $commands[] = 'git pull'; $commands[] = drush_get_option('gitpullparams'); } else { // Use a stable repository. $commands[] = 'git fetch'; $commands[] = drush_get_option('gitfetchparams'); $commands[] = ';'; $commands[] = 'git checkout'; $commands[] = drush_get_option('gitcheckoutparams'); $commands[] = $release['version']; } if (!drush_shell_cd_and_exec($request['full_project_path'], implode(' ', $commands))) { return drush_set_error('DRUSH_PM_UNABLE_CHECKOUT', 'Unable to update ' . $request['name'] . ' from git.drupal.org.'); } return TRUE; } /** * Post download action. * * This action take place once the project is placed in its final location. * * Here we add the project as a git submodule. */ function package_handler_post_download($project, $release) { if (drush_get_option('gitsubmodule', FALSE)) { // Obtain the superproject path, then add as submodule. if (drush_shell_cd_and_exec(dirname($project['full_project_path']), 'git rev-parse --show-toplevel')) { $output = drush_shell_exec_output(); $superproject = $output[0]; // Add the downloaded project as a submodule of its git superproject. $command = array(); $command[] = 'git submodule add'; $command[] = drush_get_option('gitsubmoduleaddparams'); $command[] = $project['repository']; // We need the submodule relative path. $command[] = substr(realpath($project['full_project_path']), strlen(realpath($superproject)) + 1); if (!drush_shell_cd_and_exec($superproject, implode(' ', $command))) { return drush_set_error('DRUSH_PM_GIT_CHECKOUT_PROBLEMS', dt('Unable to add !name as a git submodule of !super.', array('!name' => $project['name'], '!super' => $superproject))); } } else { return drush_set_error('DRUSH_PM_GIT_SUBMODULE_PROBLEMS', dt('Unable to create !project as a git submodule: !dir is not in a Git repository.', array('!project' => $project['name'], '!dir' => dirname($project['full_project_path'])))); } } if (drush_get_option('gitinfofile', FALSE)) { $matches = array(); if (preg_match('/^(.+).x-dev$/', $release['version'], $matches)) { $full_version = drush_pm_git_drupalorg_compute_rebuild_version($project['full_project_path'], $matches[1]); } else { $full_version = $release['version']; } if (drush_shell_cd_and_exec(dirname($project['full_project_path']), 'git log -1 --pretty=format:%ct')) { $output = drush_shell_exec_output(); $datestamp = $output[0]; } else { $datestamp = time(); } drush_pm_inject_info_file_metadata($project['full_project_path'], $project['name'], $full_version, $datestamp); } } /** * Helper function to compute the rebulid version string for a project. * * This does some magic in Git to find the latest release tag along * the branch we're packaging from, count the number of commits since * then, and use that to construct this fancy alternate version string * which is useful for the version-specific dependency support in Drupal * 7 and higher. * * NOTE: A similar function lives in git_deploy and in the drupal.org * packaging script (see DrupalorgProjectPackageRelease.class.php inside * drupalorg/drupalorg_project/plugins/release_packager). Any changes to the * actual logic in here should probably be reflected in the other places. * * @param string $project_dir * The full path to the root directory of the project to operate on. * @param string $branch * The branch that we're using for -dev. This should only include the * core version, the dash, and the branch's major version (eg. '7.x-2'). * * @return string * The full 'rebuild version string' in the given Git checkout. */ function drush_pm_git_drupalorg_compute_rebuild_version($project_dir, $branch) { $rebuild_version = ''; $branch_preg = preg_quote($branch); if (drush_shell_cd_and_exec($project_dir, 'git describe --tags')) { $shell_output = drush_shell_exec_output(); $last_tag = $shell_output[0]; // Make sure the tag starts as Drupal formatted (for eg. // 7.x-1.0-alpha1) and if we are on a proper branch (ie. not master) // then it's on that branch. if (preg_match('/^(?' . $branch_preg . '\.\d+(?:-[^-]+)?)(?-(?\d+-)g[0-9a-f]{7})?$/', $last_tag, $matches)) { // If we found additional git metadata (in particular, number of commits) // then use that info to build the version string. if (isset($matches['gitextra'])) { $rebuild_version = $matches['drupalversion'] . '+' . $matches['numberofcommits'] . 'dev'; } // Otherwise, the branch tip is pointing to the same commit as the // last tag on the branch, in which case we use the prior tag and // add '+0-dev' to indicate we're still on a -dev branch. else { $rebuild_version = $last_tag . '+0-dev'; } } } return $rebuild_version; } to download link, so it is part of the cache key. Dev snapshots can then be cached forever. $download_link = $release['download_link']; if (strpos($release['download_link'], '-dev') !== FALSE) { $download_link .= '?date=' . $release['date']; } // Cache for a year by default. $cache_duration = (drush_get_option('cache', TRUE)) ? 86400*365 : 0; // Prepare download path. On Windows file name cannot contain '?'. // See http://drupal.org/node/1782444 $filename = str_replace('?', '_', basename($download_link)); $download_path = drush_tempdir() . '/' . $filename; // Download the tarball. $download_path = drush_download_file($download_link, $download_path, $cache_duration); if ($download_path || drush_get_context('DRUSH_SIMULATE')) { drush_log(dt('Downloading !filename was successful.', array('!filename' => $filename))); } else { return drush_set_error('DRUSH_PM_DOWNLOAD_FAILED', dt('Unable to download !project to !path from !url.', array('!project' => $request['name'], '!path' => $download_path, '!url' => $download_link))); } // Check Md5 hash. if (!drush_get_option('no-md5')) { if (drush_op('md5_file', $download_path) !== $release['mdhash'] && !drush_get_context('DRUSH_SIMULATE')) { drush_delete_dir(drush_download_file_name($download_link, TRUE)); return drush_set_error('DRUSH_PM_FILE_CORRUPT', dt('File !filename is corrupt (wrong md5 checksum).', array('!filename' => $filename))); } else { drush_log(dt('Md5 checksum of !filename verified.', array('!filename' => $filename))); } } // Extract the tarball in place and return the full path to the untarred directory. $download_base = dirname($download_path); if (!$tar_file_list = drush_tarball_extract($download_path, $download_base, TRUE)) { // An error has been logged. return FALSE; } $tar_directory = drush_trim_path($tar_file_list[0]); return $download_base . '/' . $tar_directory; } /** * Update a project. * * @return bool * Success or failure. An error message will be logged. */ function package_handler_update_project(&$request, $release) { $download_path = package_handler_download_project($request, $release); if ($download_path) { return drush_move_dir($download_path, $request['full_project_path']); } else { return FALSE; } } /** * Post download action. * * This action take place once the project is placed in its final location. */ function package_handler_post_download($project) { } drush_get_context('DRUSH_SITE_WIDE_COMMANDFILES'))); } } /** * Implementation of hook_drush_command(). */ function pm_drush_command() { $update_options = array( 'lock' => array( 'description' => 'Add a persistent lock to remove the specified projects from consideration during updates. Locks may be removed with the --unlock parameter, or overridden by specifically naming the project as a parameter to pm-update or pm-updatecode. The lock does not affect pm-download. See also the update_advanced project for similar and improved functionality.', 'example-value' => 'foo,bar', ), ); $update_suboptions = array( 'lock' => array( 'lock-message' => array( 'description' => 'A brief message explaining why a project is being locked; displayed during pm-updatecode. Optional.', 'example-value' => 'message', ), 'unlock' => array( 'description' => 'Remove the persistent lock from the specified projects so that they may be updated again.', 'example-value' => 'foo,bar', ), ), ); $items['pm-enable'] = array( 'description' => 'Enable one or more extensions (modules or themes).', 'arguments' => array( 'extensions' => 'A list of modules or themes. You can use the * wildcard at the end of extension names to enable all matches.', ), 'options' => array( 'resolve-dependencies' => 'Attempt to download any missing dependencies. At the moment, only works when the module name is the same as the project name.', 'skip' => 'Skip automatic downloading of libraries (c.f. devel).', ), 'aliases' => array('en'), 'engines' => array( 'release_info' => array( 'add-options-to-command' => FALSE, ), ), ); $items['pm-disable'] = array( 'description' => 'Disable one or more extensions (modules or themes).', 'arguments' => array( 'extensions' => 'A list of modules or themes. You can use the * wildcard at the end of extension names to disable multiple matches.', ), 'aliases' => array('dis'), 'engines' => array( 'version_control', 'package_handler', 'release_info' => array( 'add-options-to-command' => FALSE, ), ), ); $items['pm-info'] = array( 'description' => 'Show detailed info for one or more extensions (modules or themes).', 'arguments' => array( 'extensions' => 'A list of modules or themes. You can use the * wildcard at the end of extension names to show info for multiple matches. If no argument is provided it will show info for all available extensions.', ), 'aliases' => array('pmi'), 'outputformat' => array( 'default' => 'key-value-list', 'pipe-format' => 'json', 'formatted-filter' => '_drush_pm_info_format_table_data', 'field-labels' => array( 'extension' => 'Extension', 'project' => 'Project', 'type' => 'Type', 'title' => 'Title', 'description' => 'Description', 'version' => 'Version', 'date' => 'Date', 'package' => 'Package', 'core' => 'Core', 'php' => 'PHP', 'status' => 'Status', 'path' => 'Path', 'schema_version' => 'Schema version', 'files' => 'Files', 'requires' => 'Requires', 'required_by' => 'Required by', 'permissions' => 'Permissions', 'config' => 'Configure', 'engine' => 'Engine', 'base_theme' => 'Base theme', 'regions' => 'Regions', 'features' => 'Features', 'stylesheets' => 'Stylesheets', // 'media_' . $media => 'Media '. $media for each $info->info['stylesheets'] as $media => $files 'scripts' => 'Scripts', ), 'output-data-type' => 'format-table', ), ); $items['pm-projectinfo'] = array( 'description' => 'Show a report of available projects and their extensions.', 'arguments' => array( 'projects' => 'Optional. A list of installed projects to show.', ), 'options' => array( 'drush' => 'Optional. Only incude projects that have one or more Drush commands.', 'status' => array( 'description' => 'Filter by project status. Choices: enabled, disabled. A project is considered enabled when at least one of its extensions is enabled.', 'example-value' => 'enabled', ), ), 'outputformat' => array( 'default' => 'key-value-list', 'pipe-format' => 'json', 'field-labels' => array( 'label' => 'Name', 'type' => 'Type', 'version' => 'Version', 'status' => 'Status', 'extensions' => 'Extensions', 'drush' => 'Drush Commands', 'datestamp' => 'Datestamp', 'path' => 'Path', ), 'fields-default' => array('label', 'type', 'version', 'status', 'extensions', 'drush', 'datestamp', 'path'), 'fields-pipe' => array('label'), 'output-data-type' => 'format-table', ), 'aliases' => array('pmpi'), ); // Install command is reserved for the download and enable of projects including dependencies. // @see http://drupal.org/node/112692 for more information. // $items['install'] = array( // 'description' => 'Download and enable one or more modules', // ); $items['pm-uninstall'] = array( 'description' => 'Uninstall one or more modules and their dependent modules.', 'arguments' => array( 'modules' => 'A list of modules.', ), 'aliases' => array('pmu'), ); $items['pm-list'] = array( 'description' => 'Show a list of available extensions (modules and themes).', 'callback arguments' => array(array(), FALSE), 'options' => array( 'type' => array( 'description' => 'Filter by extension type. Choices: module, theme.', 'example-value' => 'module', ), 'status' => array( 'description' => 'Filter by extension status. Choices: enabled, disabled and/or \'not installed\'. You can use multiple comma separated values. (i.e. --status="disabled,not installed").', 'example-value' => 'disabled', ), 'package' => 'Filter by project packages. You can use multiple comma separated values. (i.e. --package="Core - required,Other").', 'core' => 'Filter out extensions that are not in drupal core.', 'no-core' => 'Filter out extensions that are provided by drupal core.', ), 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'list', 'field-labels' => array('package' => 'Package', 'name' => 'Name', 'type' => 'Type', 'status' => 'Status', 'version' => 'Version'), 'output-data-type' => 'format-table', ), 'aliases' => array('pml'), ); $items['pm-refresh'] = array( 'description' => 'Refresh update status information.', 'engines' => array( 'update_status' => array( 'add-options-to-command' => FALSE, ), ), 'aliases' => array('rf'), ); $items['pm-updatestatus'] = array( 'description' => 'Show a report of available minor updates to Drupal core and contrib projects.', 'arguments' => array( 'projects' => 'Optional. A list of installed projects to show.', ), 'options' => array( 'pipe' => 'Return a list of the projects with any extensions enabled that need updating, one project per line.', ) + $update_options, 'sub-options' => $update_suboptions, 'engines' => array( 'update_status', ), 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'list', 'field-labels' => array('name' => 'Short Name', 'label' => 'Name', 'existing_version' => 'Installed Version', 'status' => 'Status', 'status_msg' => 'Message', 'candidate_version' => 'Proposed version'), 'fields-default' => array('label', 'existing_version', 'candidate_version', 'status_msg' ), 'fields-pipe' => array('name', 'existing_version', 'candidate_version', 'status_msg'), 'output-data-type' => 'format-table', ), 'aliases' => array('ups'), ); $items['pm-updatecode'] = array( 'description' => 'Update Drupal core and contrib projects to latest recommended releases.', 'examples' => array( 'drush pm-updatecode --no-core' => 'Update contrib projects, but skip core.', 'drush pm-updatestatus --format=csv --list-separator=" " --fields="name,existing_version,candidate_version,status_msg"' => 'To show a list of projects with their update status, use pm-updatestatus instead of pm-updatecode.', ), 'arguments' => array( 'projects' => 'Optional. A list of installed projects to update.', ), 'options' => array( 'notes' => 'Show release notes for each project to be updated.', 'no-core' => 'Only update modules and skip the core update.', 'check-updatedb' => 'Check to see if an updatedb is needed after updating the code. Default is on; use --check-updatedb=0 to disable.', ) + $update_options, 'sub-options' => $update_suboptions, 'aliases' => array('upc'), 'topics' => array('docs-policy'), 'engines' => array( 'version_control', 'package_handler', 'release_info' => array( 'add-options-to-command' => FALSE, ), 'update_status', ), ); // Merge all items from above. $items['pm-update'] = array( 'description' => 'Update Drupal core and contrib projects and apply any pending database updates (Same as pm-updatecode + updatedb).', 'aliases' => array('up'), 'allow-additional-options' => array('pm-updatecode', 'updatedb'), ); $items['pm-updatecode-postupdate'] = array( 'description' => 'Notify of pending db updates.', 'hidden' => TRUE, ); $items['pm-releasenotes'] = array( 'description' => 'Print release notes for given projects.', 'arguments' => array( 'projects' => 'A list of project names, with optional version. Defaults to \'drupal\'', ), 'options' => array( 'html' => dt('Display release notes in HTML rather than plain text.'), ), 'examples' => array( 'drush rln cck' => 'Prints the release notes for the recommended version of CCK project.', 'drush rln token-1.13' => 'View release notes of a specfic version of the Token project for my version of Drupal.', 'drush rln pathauto zen' => 'View release notes for the recommended version of Pathauto and Zen projects.', ), 'aliases' => array('rln'), 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'engines' => array( 'release_info', ), ); $items['pm-releases'] = array( 'description' => 'Print release information for given projects.', 'arguments' => array( 'projects' => 'A list of drupal.org project names. Defaults to \'drupal\'', ), 'examples' => array( 'drush pm-releases cck zen' => 'View releases for cck and Zen projects for your Drupal version.', ), 'options' => array( 'default-major' => 'Show releases compatible with the specified major version of Drupal.', ), 'aliases' => array('rl'), 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'csv', 'field-labels' => array( 'project' => 'Project', 'version' => 'Release', 'date' => 'Date', 'status' => 'Status', 'release_link' => 'Release link', 'download_link' => 'Download link', ), 'fields-default' => array('project', 'version', 'date', 'status'), 'fields-pipe' => array('project', 'version', 'date', 'status'), 'output-data-type' => 'format-table', ), 'engines' => array( 'release_info', ), ); $items['pm-download'] = array( 'description' => 'Download projects from drupal.org or other sources.', 'examples' => array( 'drush dl drupal' => 'Download latest recommended release of Drupal core.', 'drush dl drupal-7.x' => 'Download latest 7.x development version of Drupal core.', 'drush dl drupal-6' => 'Download latest recommended release of Drupal 6.x.', 'drush dl cck zen' => 'Download latest versions of CCK and Zen projects.', 'drush dl og-1.3' => 'Download a specfic version of Organic groups module for my version of Drupal.', 'drush dl diff-6.x-2.x' => 'Download a specific development branch of diff module for a specific Drupal version.', 'drush dl views --select' => 'Show a list of recent releases of the views project, prompt for which one to download.', 'drush dl webform --dev' => 'Download the latest dev release of webform.', 'drush dl webform --cache' => 'Download webform. Fetch and populate the download cache as needed.', ), 'arguments' => array( 'projects' => 'A comma delimited list of drupal.org project names, with optional version. Defaults to \'drupal\'', ), 'options' => array( 'destination' => array( 'description' => 'Path to which the project will be copied. If you\'re providing a relative path, note it is relative to the drupal root (if bootstrapped).', 'example-value' => 'path', ), 'use-site-dir' => 'Force to use the site specific directory. It will create the directory if it doesn\'t exist. If --destination is also present this option will be ignored.', 'notes' => 'Show release notes after each project is downloaded.', 'variant' => array( 'description' => "Only useful for install profiles. Possible values: 'full', 'projects', 'profile-only'.", 'example-value' => 'full', ), 'select' => "Select the version to download interactively from a list of available releases.", 'drupal-project-rename' => 'Alternate name for "drupal-x.y" directory when downloading Drupal project. Defaults to "drupal".', 'default-major' => array( 'description' => 'Specify the default major version of modules to download when there is no bootstrapped Drupal site. Defaults to "8".', 'example-value' => '7', ), 'skip' => 'Skip automatic downloading of libraries (c.f. devel).', 'pipe' => 'Returns a list of the names of the extensions (modules and themes) contained in the downloaded projects.', ), 'bootstrap' => DRUSH_BOOTSTRAP_MAX, 'aliases' => array('dl'), 'engines' => array( 'version_control', 'package_handler', 'release_info', ), ); return $items; } /** * @defgroup extensions Extensions management. * @{ * Functions to manage extensions. */ /** * Command argument complete callback. */ function pm_pm_enable_complete() { return pm_complete_extensions(); } /** * Command argument complete callback. */ function pm_pm_disable_complete() { return pm_complete_extensions(); } /** * Command argument complete callback. */ function pm_pm_uninstall_complete() { return pm_complete_extensions(); } /** * Command argument complete callback. */ function pm_pm_info_complete() { return pm_complete_extensions(); } /** * Command argument complete callback. */ function pm_pm_releasenotes_complete() { return pm_complete_projects(); } /** * Command argument complete callback. */ function pm_pm_releases_complete() { return pm_complete_projects(); } /** * Command argument complete callback. */ function pm_pm_updatecode_complete() { return pm_complete_projects(); } /** * Command argument complete callback. */ function pm_pm_update_complete() { return pm_complete_projects(); } /** * List extensions for completion. * * @return * Array of available extensions. */ function pm_complete_extensions() { if (drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { $extension_info = drush_get_extensions(FALSE); return array('values' => array_keys($extension_info)); } } /** * List projects for completion. * * @return * Array of installed projects. */ function pm_complete_projects() { if (drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { return array('values' => array_keys(drush_get_projects())); } } /** * Sort callback function for sorting extensions. * * It will sort first by type, second by package and third by name. */ function _drush_pm_sort_extensions($a, $b) { $a_type = drush_extension_get_type($a); $b_type = drush_extension_get_type($b); if ($a_type == 'module' && $b_type == 'theme') { return -1; } if ($a_type == 'theme' && $b_type == 'module') { return 1; } $cmp = strcasecmp($a->info['package'], $b->info['package']); if ($cmp == 0) { $cmp = strcasecmp($a->info['name'], $b->info['name']); } return $cmp; } /** * Calculate an extension status based on current status and schema version. * * @param $extension * Object of a single extension info. * * @return * String describing extension status. Values: enabled|disabled|not installed */ function drush_get_extension_status($extension) { if ((drush_extension_get_type($extension) == 'module') && ($extension->schema_version == -1)) { $status = "not installed"; } else { $status = ($extension->status == 1)?'enabled':'disabled'; } return $status; } /** * Classify extensions as modules, themes or unknown. * * @param $extensions * Array of extension names, by reference. * @param $modules * Empty array to be filled with modules in the provided extension list. * @param $themes * Empty array to be filled with themes in the provided extension list. */ function drush_pm_classify_extensions(&$extensions, &$modules, &$themes, $extension_info) { _drush_pm_expand_extensions($extensions, $extension_info); foreach ($extensions as $extension) { if (!isset($extension_info[$extension])) { continue; } $type = drush_extension_get_type($extension_info[$extension]); if ($type == 'module') { $modules[$extension] = $extension; } else if ($type == 'theme') { $themes[$extension] = $extension; } } } /** * Obtain an array of installed projects off the extensions available. * * A project is considered to be 'enabled' when any of its extensions is * enabled. * If any extension lacks project information and it is found that the * extension was obtained from drupal.org's cvs or git repositories, a new * 'vcs' attribute will be set on the extension. Example: * $extensions[name]->vcs = 'cvs'; * * @param array $extensions * Array of extensions as returned by drush_get_extensions(). * * @return * Array of installed projects with info of version, status and provided * extensions. */ function drush_get_projects(&$extensions = NULL) { if (!isset($extensions)) { $extensions = drush_get_extensions(); } $projects = array( 'drupal' => array( 'label' => 'Drupal', 'version' => drush_drupal_version(), 'type' => 'core', 'extensions' => array(), ) ); if (isset($extensions['system']->info['datestamp'])) { $projects['drupal']['datestamp'] = $extensions['system']->info['datestamp']; } foreach ($extensions as $extension) { $extension_name = drush_extension_get_name($extension); $extension_path = drush_extension_get_path($extension); // Obtain the project name. It is not available in this cases: // 1. the extension is part of drupal core. // 2. the project was checked out from CVS/git and cvs_deploy/git_deploy // is not installed. // 3. it is not a project hosted in drupal.org. if (empty($extension->info['project'])) { if (isset($extension->info['version']) && ($extension->info['version'] == drush_drupal_version())) { $project = 'drupal'; } else { if (is_dir($extension_path . '/CVS') && (!drush_module_exists('cvs_deploy'))) { $extension->vcs = 'cvs'; drush_log(dt('Extension !extension is fetched from cvs. Ignoring.', array('!extension' => $extension_name)), LogLevel::DEBUG); } elseif (is_dir($extension_path . '/.git') && (!drush_module_exists('git_deploy'))) { $extension->vcs = 'git'; drush_log(dt('Extension !extension is fetched from git. Ignoring.', array('!extension' => $extension_name)), LogLevel::DEBUG); } continue; } } else { $project = $extension->info['project']; } // Create/update the project in $projects with the project data. if (!isset($projects[$project])) { $projects[$project] = array( // If there's an extension with matching name, pick its label. // Otherwise use just the project name. We avoid $extension->label // for the project label because the extension's label may have // no direct relation with the project name. For example, // "Text (text)" or "Number (number)" for the CCK project. 'label' => isset($extensions[$project]) ? $extensions[$project]->label : $project, 'type' => drush_extension_get_type($extension), 'version' => $extension->info['version'], 'status' => $extension->status, 'extensions' => array(), ); if (isset($extension->info['datestamp'])) { $projects[$project]['datestamp'] = $extension->info['datestamp']; } if (isset($extension->info['project status url'])) { $projects[$project]['status url'] = $extension->info['project status url']; } } else { // If any of the extensions is enabled, consider the project is enabled. if ($extension->status != 0) { $projects[$project]['status'] = $extension->status; } } $projects[$project]['extensions'][] = drush_extension_get_name($extension); } // Obtain each project's path and try to provide a better label for ones // with machine name. $reserved = array('modules', 'sites', 'themes'); foreach ($projects as $name => $project) { if ($name == 'drupal') { continue; } // If this project has no human label, see if we can find // one "main" extension whose label we could use. if ($project['label'] == $name) { // If there is only one extension, construct a label based on // the extension name. if (count($project['extensions']) == 1) { $extension = $extensions[$project['extensions'][0]]; $projects[$name]['label'] = $extension->info['name'] . ' (' . $name . ')'; } else { // Make a list of all of the extensions in this project // that do not depend on any other extension in this // project. $candidates = array(); foreach ($project['extensions'] as $e) { $has_project_dependency = FALSE; if (isset($extensions[$e]->info['dependencies']) && is_array($extensions[$e]->info['dependencies'])) { foreach ($extensions[$e]->info['dependencies'] as $dependent) { if (in_array($dependent, $project['extensions'])) { $has_project_dependency = TRUE; } } } if ($has_project_dependency === FALSE) { $candidates[] = $extensions[$e]->info['name']; } } // If only one of the modules is a candidate, use its name in the label if (count($candidates) == 1) { $projects[$name]['label'] = reset($candidates) . ' (' . $name . ')'; } } } drush_log(dt('Obtaining !project project path.', array('!project' => $name)), LogLevel::DEBUG); $path = _drush_pm_find_common_path($project['type'], $project['extensions']); // Prevent from setting a reserved path. For example it may happen in a case // where a module and a theme are declared as part of a same project. // There's a special case, a project called "sites", this is the reason for // the second condition here. if ($path == '.' || (in_array(basename($path), $reserved) && !in_array($name, $reserved))) { drush_log(dt('Error while trying to find the common path for enabled extensions of project !project. Extensions are: !extensions.', array('!project' => $name, '!extensions' => implode(', ', $project['extensions']))), LogLevel::ERROR); } else { $projects[$name]['path'] = $path; } } return $projects; } /** * Helper function to find the common path for a list of extensions in the aim to obtain the project name. * * @param $project_type * Type of project we're trying to find. Valid values: module, theme. * @param $extensions * Array of extension names. */ function _drush_pm_find_common_path($project_type, $extensions) { // Select the first path as the candidate to be the common prefix. $extension = array_pop($extensions); while (!($path = drupal_get_path($project_type, $extension))) { drush_log(dt('Unknown path for !extension !type.', array('!extension' => $extension, '!type' => $project_type)), LogLevel::WARNING); $extension = array_pop($extensions); } // If there's only one extension we are done. Otherwise, we need to find // the common prefix for all of them. if (count($extensions) > 0) { // Iterate over the other projects. while($extension = array_pop($extensions)) { $path2 = drupal_get_path($project_type, $extension); if (!$path2) { drush_log(dt('Unknown path for !extension !type.', array('!extension' => $extension, '!type' => $project_type)), LogLevel::DEBUG); continue; } // Option 1: same path. if ($path == $path2) { continue; } // Option 2: $path is a prefix of $path2. if (strpos($path2, $path) === 0) { continue; } // Option 3: $path2 is a prefix of $path. if (strpos($path, $path2) === 0) { $path = $path2; continue; } // Option 4: no one is a prefix of the other. Find the common // prefix by iteratively strip the rigthtmost piece of $path. // We will iterate until a prefix is found or path = '.', that on the // other hand is a condition theorically impossible to reach. do { $path = dirname($path); if (strpos($path2, $path) === 0) { break; } } while ($path != '.'); } } return $path; } /** * @} End of "defgroup extensions". */ /** * Command callback. Show a list of extensions with type and status. */ function drush_pm_list() { //--package $package_filter = array(); $package = strtolower(drush_get_option('package')); if (!empty($package)) { $package_filter = explode(',', $package); } if (!empty($package_filter) && (count($package_filter) == 1)) { drush_hide_output_fields('package'); } //--type $all_types = array('module', 'theme'); $type_filter = strtolower(drush_get_option('type')); if (!empty($type_filter)) { $type_filter = explode(',', $type_filter); } else { $type_filter = $all_types; } if (count($type_filter) == 1) { drush_hide_output_fields('type'); } foreach ($type_filter as $type) { if (!in_array($type, $all_types)) { //TODO: this kind of check can be implemented drush-wide return drush_set_error('DRUSH_PM_INVALID_PROJECT_TYPE', dt('!type is not a valid project type.', array('!type' => $type))); } } //--status $all_status = array('enabled', 'disabled', 'not installed'); $status_filter = strtolower(drush_get_option('status')); if (!empty($status_filter)) { $status_filter = explode(',', $status_filter); } else { $status_filter = $all_status; } if (count($status_filter) == 1) { drush_hide_output_fields('status'); } foreach ($status_filter as $status) { if (!in_array($status, $all_status)) { //TODO: this kind of check can be implemented drush-wide return drush_set_error('DRUSH_PM_INVALID_PROJECT_STATUS', dt('!status is not a valid project status.', array('!status' => $status))); } } $result = array(); $extension_info = drush_get_extensions(FALSE); uasort($extension_info, '_drush_pm_sort_extensions'); $major_version = drush_drupal_major_version(); foreach ($extension_info as $key => $extension) { if (!in_array(drush_extension_get_type($extension), $type_filter)) { unset($extension_info[$key]); continue; } $status = drush_get_extension_status($extension); if (!in_array($status, $status_filter)) { unset($extension_info[$key]); continue; } // Filter out core if --no-core specified. if (drush_get_option('no-core', FALSE)) { if ((($major_version >= 8) && ($extension->origin == 'core')) || (($major_version <= 7) && (strpos($extension->info['package'], 'Core') === 0))) { unset($extension_info[$key]); continue; } } // Filter out non-core if --core specified. if (drush_get_option('core', FALSE)) { if ((($major_version >= 8) && ($extension->origin != 'core')) || (($major_version <= 7) && (strpos($extension->info['package'], 'Core') !== 0))) { unset($extension_info[$key]); continue; } } // Filter by package. if (!empty($package_filter)) { if (!in_array(strtolower($extension->info['package']), $package_filter)) { unset($extension_info[$key]); continue; } } $row['package'] = $extension->info['package']; $row['name'] = $extension->label; $row['type'] = ucfirst(drush_extension_get_type($extension)); $row['status'] = ucfirst($status); // Suppress notice when version is not present. $row['version'] = @$extension->info['version']; $result[$key] = $row; unset($row); } // In Drush-5, we used to return $extension_info here. return $result; } /** * Helper function for pm-enable. */ function drush_pm_enable_find_project_from_extension($extension) { $result = drush_pm_lookup_extension_in_cache($extension); if (!isset($result)) { $release_info = drush_get_engine('release_info'); // If we can find info on a project that has the same name // as the requested extension, then we'll call that a match. $request = pm_parse_request($extension); if ($release_info->checkProject($request)) { $result = $extension; } } return $result; } /** * Validate callback. Determine the modules and themes that the user would like enabled. */ function drush_pm_enable_validate() { $args = pm_parse_arguments(func_get_args()); $extension_info = drush_get_extensions(); $recheck = TRUE; $last_download = NULL; while ($recheck) { $recheck = FALSE; // Classify $args in themes, modules or unknown. $modules = array(); $themes = array(); $download = array(); drush_pm_classify_extensions($args, $modules, $themes, $extension_info); $extensions = array_merge($modules, $themes); $unknown = array_diff($args, $extensions); // If there're unknown extensions, try and download projects // with matching names. if (!empty($unknown)) { $found = array(); foreach ($unknown as $name) { drush_log(dt('!extension was not found.', array('!extension' => $name)), LogLevel::WARNING); $project = drush_pm_enable_find_project_from_extension($name); if (!empty($project)) { $found[] = $project; } } if (!empty($found)) { // Prevent from looping if last download failed. if ($found === $last_download) { drush_log(dt("Unable to download some or all of the extensions."), LogLevel::WARNING); break; } drush_log(dt("The following projects provide some or all of the extensions not found:\n@list", array('@list' => implode("\n", $found))), LogLevel::OK); if (drush_get_option('resolve-dependencies')) { drush_log(dt("They are being downloaded."), LogLevel::OK); } if ((drush_get_option('resolve-dependencies')) || (drush_confirm("Would you like to download them?"))) { $download = $found; } } } // Discard already enabled and incompatible extensions. foreach ($extensions as $name) { if ($extension_info[$name]->status) { drush_log(dt('!extension is already enabled.', array('!extension' => $name)), LogLevel::OK); } // Check if the extension is compatible with Drupal core and php version. if ($component = drush_extension_check_incompatibility($extension_info[$name])) { drush_set_error('DRUSH_PM_ENABLE_MODULE_INCOMPATIBLE', dt('!name is incompatible with the !component version.', array('!name' => $name, '!component' => $component))); if (drush_extension_get_type($extension_info[$name]) == 'module') { unset($modules[$name]); } else { unset($themes[$name]); } } } if (!empty($modules)) { // Check module dependencies. $dependencies = drush_check_module_dependencies($modules, $extension_info); $unmet_dependencies = array(); foreach ($dependencies as $module => $info) { if (!empty($info['unmet-dependencies'])) { foreach ($info['unmet-dependencies'] as $unmet) { $unmet_project = (!empty($info['dependencies'][$unmet]['project'])) ? $info['dependencies'][$unmet]['project'] : drush_pm_enable_find_project_from_extension($unmet); if (!empty($unmet_project)) { $unmet_dependencies[$module][$unmet_project] = $unmet_project; } } } } if (!empty($unmet_dependencies)) { $msgs = array(); $unmet_project_list = array(); foreach ($unmet_dependencies as $module => $unmet_projects) { $unmet_project_list = array_merge($unmet_project_list, $unmet_projects); $msgs[] = dt("!module requires !unmet-projects", array('!unmet-projects' => implode(', ', $unmet_projects), '!module' => $module)); } $found = array_merge($download, $unmet_project_list); // Prevent from looping if last download failed. if ($found === $last_download) { drush_log(dt("Unable to download some or all of the extensions."), LogLevel::WARNING); break; } drush_log(dt("The following projects have unmet dependencies:\n!list", array('!list' => implode("\n", $msgs))), LogLevel::OK); if (drush_get_option('resolve-dependencies')) { drush_log(dt("They are being downloaded."), LogLevel::OK); } if (drush_get_option('resolve-dependencies') || drush_confirm(dt("Would you like to download them?"))) { $download = $found; } } } if (!empty($download)) { // Disable DRUSH_AFFIRMATIVE context temporarily. $drush_affirmative = drush_get_context('DRUSH_AFFIRMATIVE'); drush_set_context('DRUSH_AFFIRMATIVE', FALSE); // Invoke a new process to download dependencies. $result = drush_invoke_process('@self', 'pm-download', $download, array(), array('interactive' => TRUE)); // Restore DRUSH_AFFIRMATIVE context. drush_set_context('DRUSH_AFFIRMATIVE', $drush_affirmative); // Refresh module cache after downloading the new modules. if (drush_drupal_major_version() >= 8) { \Drush\Drupal\ExtensionDiscovery::reset(); system_list_reset(); } $extension_info = drush_get_extensions(); $last_download = $download; $recheck = TRUE; } } if (!empty($modules)) { $all_dependencies = array(); $dependencies_ok = TRUE; foreach ($dependencies as $key => $info) { if (isset($info['error'])) { unset($modules[$key]); $dependencies_ok = drush_set_error($info['error']['code'], $info['error']['message']); } elseif (!empty($info['dependencies'])) { // Make sure we have an assoc array. $dependencies_list = array_keys($info['dependencies']); $assoc = array_combine($dependencies_list, $dependencies_list); $all_dependencies = array_merge($all_dependencies, $assoc); } } if (!$dependencies_ok) { return FALSE; } $modules = array_diff(array_merge($modules, $all_dependencies), drush_module_list()); // Discard modules which doesn't meet requirements. require_once DRUSH_DRUPAL_CORE . '/includes/install.inc'; foreach ($modules as $key => $module) { // Check to see if the module can be installed/enabled (hook_requirements). // See @system_modules_submit if (!drupal_check_module($module)) { unset($modules[$key]); drush_set_error('DRUSH_PM_ENABLE_MODULE_UNMEET_REQUIREMENTS', dt('Module !module doesn\'t meet the requirements to be enabled.', array('!module' => $module))); _drush_log_drupal_messages(); return FALSE; } } } $searchpath = array(); foreach (array_merge($modules, $themes) as $name) { $searchpath[] = drush_extension_get_path($extension_info[$name]); } // Add all modules that passed validation to the drush // list of commandfiles (if they have any). This // will allow these newly-enabled modules to participate // in the pre-pm_enable and post-pm_enable hooks. if (!empty($searchpath)) { _drush_add_commandfiles($searchpath); } drush_set_context('PM_ENABLE_EXTENSION_INFO', $extension_info); drush_set_context('PM_ENABLE_MODULES', $modules); drush_set_context('PM_ENABLE_THEMES', $themes); return TRUE; } /** * Command callback. Enable one or more extensions from downloaded projects. * Note that the modules and themes to be enabled were evaluated during the * pm-enable validate hook, above. */ function drush_pm_enable() { // Get the data built during the validate phase $extension_info = drush_get_context('PM_ENABLE_EXTENSION_INFO'); $modules = drush_get_context('PM_ENABLE_MODULES'); $themes = drush_get_context('PM_ENABLE_THEMES'); // Inform the user which extensions will finally be enabled. $extensions = array_merge($modules, $themes); if (empty($extensions)) { return drush_log(dt('There were no extensions that could be enabled.'), LogLevel::OK); } else { drush_print(dt('The following extensions will be enabled: !extensions', array('!extensions' => implode(', ', $extensions)))); if(!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } } // Enable themes. if (!empty($themes)) { drush_theme_enable($themes); } // Enable modules and pass dependency validation in form submit. if (!empty($modules)) { drush_include_engine('drupal', 'environment'); drush_module_enable($modules); } // Inform the user of final status. $result_extensions = drush_get_named_extensions_list($extensions); $problem_extensions = array(); $role = drush_role_get_class(); foreach ($result_extensions as $name => $extension) { if ($extension->status) { drush_log(dt('!extension was enabled successfully.', array('!extension' => $name)), LogLevel::OK); $perms = $role->getModulePerms($name); if (!empty($perms)) { drush_print(dt('!extension defines the following permissions: !perms', array('!extension' => $name, '!perms' => implode(', ', $perms)))); } } else { $problem_extensions[] = $name; } } if (!empty($problem_extensions)) { return drush_set_error('DRUSH_PM_ENABLE_EXTENSION_ISSUE', dt('There was a problem enabling !extension.', array('!extension' => implode(',', $problem_extensions)))); } // Return the list of extensions enabled return $extensions; } /** * Command callback. Disable one or more extensions. */ function drush_pm_disable() { $args = pm_parse_arguments(func_get_args()); drush_include_engine('drupal', 'pm'); _drush_pm_disable($args); } /** * Add extensions that match extension_name*. * * A helper function for commands that take a space separated list of extension * names. It will identify extensions that have been passed in with a * trailing * and add all matching extensions to the array that is returned. * * @param $extensions * An array of extensions, by reference. * @param $extension_info * Optional. An array of extension info as returned by drush_get_extensions(). */ function _drush_pm_expand_extensions(&$extensions, $extension_info = array()) { if (empty($extension_info)) { $extension_info = drush_get_extensions(); } foreach ($extensions as $key => $extension) { if (($wildcard = rtrim($extension, '*')) !== $extension) { foreach (array_keys($extension_info) as $extension_name) { if (substr($extension_name, 0, strlen($wildcard)) == $wildcard) { $extensions[] = $extension_name; } } unset($extensions[$key]); continue; } } } /** * Command callback. Uninstall one or more modules. */ function drush_pm_uninstall() { $args = pm_parse_arguments(func_get_args()); drush_include_engine('drupal', 'pm'); _drush_pm_uninstall($args); } /** * Command callback. Show available releases for given project(s). */ function drush_pm_releases() { $release_info = drush_get_engine('release_info'); // Obtain requests. $requests = pm_parse_arguments(func_get_args(), FALSE); if (!$requests) { $requests = array('drupal'); } // Get installed projects. if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) { $projects = drush_get_projects(); } else { $projects = array(); } // Select the filter to apply based on cli options. if (drush_get_option('dev', FALSE)) { $filter = 'dev'; } elseif (drush_get_option('all', FALSE)) { $filter = 'all'; } else { $filter = ''; } $status_url = drush_get_option('source'); $output = array(); foreach ($requests as $request) { $request = pm_parse_request($request, $status_url, $projects); $project_name = $request['name']; $project_release_info = $release_info->get($request); if ($project_release_info) { $version = isset($projects[$project_name]) ? $projects[$project_name]['version'] : NULL; $releases = $project_release_info->filterReleases($filter, $version); foreach ($releases as $key => $release) { $output["${project_name}-${key}"] = array( 'project' => $project_name, 'version' => $release['version'], 'date' => gmdate('Y-M-d', $release['date']), 'status' => implode(', ', $release['release_status']), ) + $release; } } } if (empty($output)) { return drush_log(dt('No valid projects given.'), LogLevel::OK); } return $output; } /** * Command callback. Show release notes for given project(s). */ function drush_pm_releasenotes() { $release_info = drush_get_engine('release_info'); // Obtain requests. if (!$requests = pm_parse_arguments(func_get_args(), FALSE)) { $requests = array('drupal'); } // Get installed projects. if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') >= DRUSH_BOOTSTRAP_DRUPAL_FULL) { $projects = drush_get_projects(); } else { $projects = array(); } $status_url = drush_get_option('source'); $output = ''; foreach($requests as $request) { $request = pm_parse_request($request, $status_url, $projects); $project_release_info = $release_info->get($request); if ($project_release_info) { $version = empty($request['version']) ? NULL : $request['version']; $output .= $project_release_info->getReleaseNotes($version); } } return $output; } /** * Command callback. Refresh update status information. */ function drush_pm_refresh() { $update_status = drush_get_engine('update_status'); drush_print(dt("Refreshing update status information ...")); $update_status->refresh(); drush_print(dt("Done.")); } /** * Command callback. Execute pm-update. */ function drush_pm_update() { // Call pm-updatecode. updatedb will be called in the post-update process. $args = pm_parse_arguments(func_get_args(), FALSE); drush_set_option('check-updatedb', FALSE); return drush_invoke('pm-updatecode', $args); } /** * Post-command callback. * Execute updatedb command after an updatecode - user requested `update`. */ function drush_pm_post_pm_update() { // Use drush_invoke_process to start a subprocess. Cleaner that way. if (drush_get_context('DRUSH_PM_UPDATED', FALSE) !== FALSE) { drush_invoke_process('@self', 'updatedb'); } } /** * Validate callback for updatecode command. Abort if 'backup' directory exists. */ function drush_pm_updatecode_validate() { $path = drush_get_context('DRUSH_DRUPAL_ROOT') . '/backup'; if (is_dir($path) && (realpath(drush_get_option('backup-dir', FALSE)) != $path)) { return drush_set_error('', dt('Backup directory !path found. It\'s a security risk to store backups inside the Drupal tree. Drush now uses by default ~/drush-backups. You need to move !path out of the Drupal tree to proceed. Note: if you know what you\'re doing you can explicitly set --backup-dir to !path and continue.', array('!path' => $path))); } } /** * Post-command callback for updatecode. * * Execute pm-updatecode-postupdate in a backend process to not conflict with * old code already in memory. */ function drush_pm_post_pm_updatecode() { // Skip if updatecode was invoked by pm-update. // This way we avoid being noisy, as updatedb is to be executed. if (drush_get_option('check-updatedb', TRUE)) { if (drush_get_context('DRUSH_PM_UPDATED', FALSE)) { drush_invoke_process('@self', 'pm-updatecode-postupdate'); } } } /** * Command callback. Execute updatecode-postupdate. */ function drush_pm_updatecode_postupdate() { // Clear the cache, since some projects could have moved around. drush_drupal_cache_clear_all(); // Notify of pending database updates. // Make sure the installation API is available require_once DRUSH_DRUPAL_CORE . '/includes/install.inc'; // Load all .install files. drupal_load_updates(); // @see system_requirements(). foreach (drush_module_list() as $module) { $updates = drupal_get_schema_versions($module); if ($updates !== FALSE) { $default = drupal_get_installed_schema_version($module); if (max($updates) > $default) { drush_log(dt("You have pending database updates. Run `drush updatedb` or visit update.php in your browser."), LogLevel::WARNING); break; } } } } /** * Sanitize user provided arguments to several pm commands. * * Return an array of arguments off a space and/or comma separated values. */ function pm_parse_arguments($args, $dashes_to_underscores = TRUE) { $arguments = _convert_csv_to_array($args); foreach ($arguments as $key => $argument) { $argument = ($dashes_to_underscores) ? strtr($argument, '-', '_') : $argument; } return $arguments; } /** * Decompound a version string and returns major, minor, patch and extra parts. * * @see _pm_parse_version_compound() * @see pm_parse_version() * * @param string $version * A version string like X.Y-Z, X.Y.Z-W or a subset. * * @return array * Array with major, patch and extra keys. */ function _pm_parse_version_decompound($version) { $pattern = '/^(\d+)(?:.(\d+))?(?:\.(x|\d+))?(?:-([a-z0-9\.-]*))?(?:\+(\d+)-dev)?$/'; $matches = array(); preg_match($pattern, $version, $matches); $parts = array( 'major' => '', 'minor' => '', 'patch' => '', 'extra' => '', 'offset' => '', ); if (isset($matches[1])) { $parts['major'] = $matches[1]; if (isset($matches[2])) { if (isset($matches[3]) && $matches[3] != '') { $parts['minor'] = $matches[2]; $parts['patch'] = $matches[3]; } else { $parts['patch'] = $matches[2]; } } if (!empty($matches[4])) { $parts['extra'] = $matches[4]; } if (!empty($matches[5])) { $parts['offset'] = $matches[5]; } } return $parts; } /** * Build a version string from an array of major, minor and extra parts. * * @see _pm_parse_version_decompound() * @see pm_parse_version() * * @param array $parts * Array of parts. * * @return string * A Version string. */ function _pm_parse_version_compound($parts) { $project_version = ''; if ($parts['patch'] != '') { $project_version = $parts['major']; if ($parts['minor'] != '') { $project_version = $project_version . '.' . $parts['minor']; } if ($parts['patch'] == 'x') { $project_version = $project_version . '.x-dev'; } else { $project_version = $project_version . '.' . $parts['patch']; if ($parts['extra'] != '') { $project_version = $project_version . '-' . $parts['extra']; } } if ($parts['offset'] != '') { $project_version = $project_version . '+' . $parts['offset'] . '-dev'; } } return $project_version; } /** * Parses a version string and returns its components. * * It parses both core and contrib version strings. * * Core (semantic versioning): * - 8.0.0-beta3+252-dev * - 8.0.0-beta2 * - 8.0.x-dev * - 8.1.x * - 8.0.1 * - 8 * * Core (classic drupal scheme): * - 7.x-dev * - 7.x * - 7.33 * - 7.34+3-dev * - 7 * * Contrib: * - 7.x-1.0-beta1+30-dev * - 7.x-1.0-beta1 * - 7.x-1.0+30-dev * - 7.x-1.0 * - 1.0-beta1 * - 1.0 * - 7.x-1.x * - 7.x-1.x-dev * - 1.x * * @see pm_parse_request() * * @param string $version * A core or project version string. * * @param bool $is_core * Whether this is a core version or a project version. * * @return array * Version string in parts. * Example for a contrib version (ex: 7.x-3.2-beta1): * - version : Fully qualified version string. * - drupal_version : Core compatibility version (ex: 7.x). * - version_major : Major version (ex: 3). * - version_minor : Minor version. Not applicable. Always empty. * - version_patch : Patch version (ex: 2). * - version_extra : Extra version (ex: beta1). * - project_version : Project specific part of the version (ex: 3.2-beta1). * * Example for a core version (ex: 8.1.2-beta2 or 7.0-beta2): * - version : Fully qualified version string. * - drupal_version : Core compatibility version (ex: 8.x). * - version_major : Major version (ex: 8). * - version_minor : Minor version (ex: 1). Empty if not a semver. * - version_patch : Patch version (ex: 2). * - version_extra : Extra version (ex: beta2). * - project_version : Same as 'version'. */ function pm_parse_version($version, $is_core = FALSE) { $core_parts = _pm_parse_version_decompound($version); // If no major version, we have no version at all. Pick a default. $drupal_version_default = drush_drupal_major_version(); if ($core_parts['major'] == '') { $core_parts['major'] = ($drupal_version_default) ? $drupal_version_default : drush_get_option('default-major', 8); } if ($is_core) { $project_version = _pm_parse_version_compound($core_parts); $version_parts = array( 'version' => $project_version, 'drupal_version' => $core_parts['major'] . '.x', 'project_version' => $project_version, 'version_major' => $core_parts['major'], 'version_minor' => $core_parts['minor'], 'version_patch' => ($core_parts['patch'] == 'x') ? '' : $core_parts['patch'], 'version_extra' => ($core_parts['patch'] == 'x') ? 'dev' : $core_parts['extra'], 'version_offset' => $core_parts['offset'], ); } else { // If something as 7.x-1.0-beta1, the project specific version is // in $version['extra'] and we need to parse it. if (strpbrk($core_parts['extra'], '.-')) { $nocore_parts = _pm_parse_version_decompound($core_parts['extra']); $nocore_parts['offset'] = $core_parts['offset']; $project_version = _pm_parse_version_compound($nocore_parts); $version_parts = array( 'version' => $core_parts['major'] . '.x-' . $project_version, 'drupal_version' => $core_parts['major'] . '.x', 'project_version' => $project_version, 'version_major' => $nocore_parts['major'], 'version_minor' => $core_parts['minor'], 'version_patch' => ($nocore_parts['patch'] == 'x') ? '' : $nocore_parts['patch'], 'version_extra' => ($nocore_parts['patch'] == 'x') ? 'dev' : $nocore_parts['extra'], 'version_offset' => $core_parts['offset'], ); } // At this point we have half a version and must decide if this is a drupal major or a project. else { // If working on a bootstrapped site, core_parts has the project version. if ($drupal_version_default) { $project_version = _pm_parse_version_compound($core_parts); $version = ($project_version) ? $drupal_version_default . '.x-' . $project_version : ''; $version_parts = array( 'version' => $version, 'drupal_version' => $drupal_version_default . '.x', 'project_version' => $project_version, 'version_major' => $core_parts['major'], 'version_minor' => $core_parts['minor'], 'version_patch' => ($core_parts['patch'] == 'x') ? '' : $core_parts['patch'], 'version_extra' => ($core_parts['patch'] == 'x') ? 'dev' : $core_parts['extra'], 'version_offset' => $core_parts['offset'], ); } // Not working on a bootstrapped site, core_parts is core version. else { $version_parts = array( 'version' => '', 'drupal_version' => $core_parts['major'] . '.x', 'project_version' => '', 'version_major' => '', 'version_minor' => '', 'version_patch' => '', 'version_extra' => '', 'version_offset' => '', ); } } } return $version_parts; } /** * Parse out the project name and version and return as a structured array. * * @see pm_parse_version() * * @param string $request_string * Project name with optional version. Examples: 'ctools-7.x-1.0-beta1' * * @return array * Array with all parts of the request info. */ function pm_parse_request($request_string, $status_url = NULL, &$projects = array()) { // Split $request_string in project name and version. Note that hyphens (-) // are permitted in project names (ex: field-conditional-state). // We use a regex to split the string. The pattern used matches a string // starting with hyphen, followed by one or more numbers, any of the valid // symbols in version strings (.x-) and a catchall for the rest of the // version string. $parts = preg_split('/-(?:([\d+\.x].*))?$/', $request_string, NULL, PREG_SPLIT_DELIM_CAPTURE); if (count($parts) == 1) { // No version in the request string. $project = $request_string; $version = ''; } else { $project = $parts[0]; $version = $parts[1]; } $is_core = ($project == 'drupal'); $request = array( 'name' => $project, ) + pm_parse_version($version, $is_core); // Set the status url if provided or available in project's info file. if ($status_url) { $request['status url'] = $status_url; } elseif (!empty($projects[$project]['status url'])) { $request['status url'] = $projects[$project]['status url']; } return $request; } /** * @defgroup engines Engine types * @{ */ /** * Implementation of hook_drush_engine_type_info(). */ function pm_drush_engine_type_info() { return array( 'package_handler' => array( 'option' => 'package-handler', 'description' => 'Determine how to fetch projects from update service.', 'default' => 'wget', 'options' => array( 'cache' => 'Cache release XML and tarballs or git clones. Git clones use git\'s --reference option. Defaults to 1 for downloads, and 0 for git.', ), ), 'release_info' => array( 'add-options-to-command' => TRUE, ), 'update_status' => array( 'option' => 'update-backend', 'description' => 'Determine how to fetch update status information.', 'default' => 'drush', 'add-options-to-command' => TRUE, 'options' => array( 'update-backend' => 'Backend to obtain available updates.', 'check-disabled' => 'Check for updates of disabled modules and themes.', 'security-only' => 'Only update modules that have security updates available.', ), 'combine-help' => TRUE, ), 'version_control' => array( 'option' => 'version-control', 'default' => 'backup', 'description' => 'Integrate with version control systems.', ), ); } /** * Implements hook_drush_engine_ENGINE_TYPE(). * * Package handler engine is used by pm-download and * pm-updatecode commands to determine how to download/checkout * new projects and acquire updates to projects. */ function pm_drush_engine_package_handler() { return array( 'wget' => array( 'description' => 'Download project packages using wget or curl.', 'options' => array( 'no-md5' => 'Skip md5 validation of downloads.', ), ), 'git_drupalorg' => array( 'description' => 'Use git.drupal.org to checkout and update projects.', 'options' => array( 'gitusername' => 'Your git username as shown on user/[uid]/edit/git. Typically, this is set this in drushrc.php. Omitting this prevents users from pushing changes back to git.drupal.org.', 'gitsubmodule' => 'Use git submodules for checking out new projects. Existing git checkouts are unaffected, and will continue to (not) use submodules regardless of this setting.', 'gitcheckoutparams' => 'Add options to the `git checkout` command.', 'gitcloneparams' => 'Add options to the `git clone` command.', 'gitfetchparams' => 'Add options to the `git fetch` command.', 'gitpullparams' => 'Add options to the `git pull` command.', 'gitinfofile' => 'Inject version info into each .info file.', ), 'sub-options' => array( 'gitsubmodule' => array( 'gitsubmoduleaddparams' => 'Add options to the `git submodule add` command.', ), ), ), ); } /** * Implements hook_drush_engine_ENGINE_TYPE(). * * Release info engine is used by several pm commands to obtain * releases info from Drupal's update service or external sources. */ function pm_drush_engine_release_info() { return array( 'updatexml' => array( 'description' => 'Drush release info engine for update.drupal.org and compatible services.', 'options' => array( 'source' => 'The base URL which provides project release history in XML. Defaults to http://updates.drupal.org/release-history.', 'dev' => 'Work with development releases solely.', ), 'sub-options' => array( 'cache' => array( 'cache-duration-releasexml' => 'Expire duration (in seconds) for release XML. Defaults to 86400 (24 hours).', ), 'select' => array( 'all' => 'Shows all available releases instead of a short list of recent releases.', ), ), 'class' => 'Drush\UpdateService\ReleaseInfo', ), ); } /** * Implements hook_drush_engine_ENGINE_TYPE(). * * Update status engine is used to check available updates for * the projects in a Drupal site. */ function pm_drush_engine_update_status() { return array( 'drupal' => array( 'description' => 'Check available updates with update.module.', 'drupal dependencies' => array('update'), 'class' => 'Drush\UpdateService\StatusInfoDrupal', ), 'drush' => array( 'description' => 'Check available updates without update.module.', 'class' => 'Drush\UpdateService\StatusInfoDrush', ), ); } /** * Implements hook_drush_engine_ENGINE_TYPE(). * * Integration with VCS in order to easily commit your changes to projects. */ function pm_drush_engine_version_control() { return array( 'backup' => array( 'description' => 'Backup all project files before updates.', 'options' => array( 'no-backup' => 'Do not perform backups. WARNING: Will result in non-core files/dirs being deleted (e.g. .git)', 'backup-dir' => 'Specify a directory to backup projects into. Defaults to drush-backups within the home directory of the user running the command. It is forbidden to specify a directory inside your drupal root.', ), ), 'bzr' => array( 'signature' => 'bzr root %s', 'description' => 'Quickly add/remove/commit your project changes to Bazaar.', 'options' => array( 'bzrsync' => 'Automatically add new files to the Bazaar repository and remove deleted files. Caution.', 'bzrcommit' => 'Automatically commit changes to Bazaar repository. You must also use the --bzrsync option.', ), 'sub-options' => array( 'bzrcommit' => array( 'bzrmessage' => 'Override default commit message which is: Drush automatic commit. Project Command: ', ), ), 'examples' => array( 'drush dl cck --version-control=bzr --bzrsync --bzrcommit' => 'Download the cck project and then add it and commit it to Bazaar.' ), ), 'svn' => array( 'signature' => 'svn info %s', 'description' => 'Quickly add/remove/commit your project changes to Subversion.', 'options' => array( 'svnsync' => 'Automatically add new files to the SVN repository and remove deleted files. Caution.', 'svncommit' => 'Automatically commit changes to SVN repository. You must also using the --svnsync option.', 'svnstatusparams' => "Add options to the 'svn status' command", 'svnaddparams' => 'Add options to the `svn add` command', 'svnremoveparams' => 'Add options to the `svn remove` command', 'svnrevertparams' => 'Add options to the `svn revert` command', 'svncommitparams' => 'Add options to the `svn commit` command', ), 'sub-options' => array( 'svncommit' => array( 'svnmessage' => 'Override default commit message which is: Drush automatic commit: ', ), ), 'examples' => array( 'drush [command] cck --svncommitparams=\"--username joe\"' => 'Commit changes as the user \'joe\' (Quotes are required).' ), ), ); } /** * @} End of "Engine types". */ /** * Interface for version control systems. * We use a simple object layer because we conceivably need more than one * loaded at a time. */ interface drush_version_control { function pre_update(&$project); function rollback($project); function post_update($project); function post_download($project); static function reserved_files(); } /** * A simple factory function that tests for version control systems, in a user * specified order, and returns the one that appears to be appropriate for a * specific directory. */ function drush_pm_include_version_control($directory = '.') { $engine_info = drush_get_engines('version_control'); $version_controls = drush_get_option('version-control', FALSE); // If no version control was given, use a list of defaults. if (!$version_controls) { // Backup engine is the last option. $version_controls = array_reverse(array_keys($engine_info['engines'])); } else { $version_controls = array($version_controls); } // Find the first valid engine in the list, checking signatures if needed. $engine = FALSE; while (!$engine && count($version_controls)) { $version_control = array_shift($version_controls); if (isset($engine_info['engines'][$version_control])) { if (!empty($engine_info['engines'][$version_control]['signature'])) { drush_log(dt('Verifying signature for !vcs version control engine.', array('!vcs' => $version_control)), LogLevel::DEBUG); if (drush_shell_exec($engine_info['engines'][$version_control]['signature'], $directory)) { $engine = $version_control; } } else { $engine = $version_control; } } } if (!$engine) { return drush_set_error('DRUSH_PM_NO_VERSION_CONTROL', dt('No valid version control or backup engine found (the --version-control option was set to "!version-control").', array('!version-control' => $version_control))); } $instance = drush_include_engine('version_control', $engine); return $instance; } /** * Update the locked status of all of the candidate projects * to be updated. * * @param array &$projects * The projects array from pm_updatecode. $project['locked'] will * be set for every file where a persistent lockfile can be found. * The 'lock' and 'unlock' operations are processed first. * @param array $projects_to_lock * A list of projects to create peristent lock files for * @param array $projects_to_unlock * A list of projects to clear the persistent lock on * @param string $lock_message * The reason the project is being locked; stored in the lockfile. * * @return array * A list of projects that are locked. */ function drush_pm_update_lock(&$projects, $projects_to_lock, $projects_to_unlock, $lock_message = NULL) { $locked_result = array(); // Warn about ambiguous lock / unlock values if ($projects_to_lock == array('1')) { $projects_to_lock = array(); drush_log(dt('Ignoring --lock with no value.'), LogLevel::WARNING); } if ($projects_to_unlock == array('1')) { $projects_to_unlock = array(); drush_log(dt('Ignoring --unlock with no value.'), LogLevel::WARNING); } // Log if we are going to lock or unlock anything if (!empty($projects_to_unlock)) { drush_log(dt('Unlocking !projects', array('!projects' => implode(',', $projects_to_unlock))), LogLevel::OK); } if (!empty($projects_to_lock)) { drush_log(dt('Locking !projects', array('!projects' => implode(',', $projects_to_lock))), LogLevel::OK); } $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT'); foreach ($projects as $name => $project) { $message = NULL; if (isset($project['path'])) { if ($name == 'drupal') { $lockfile = $drupal_root . '/.drush-lock-update'; } else { $lockfile = $drupal_root . '/' . $project['path'] . '/.drush-lock-update'; } // Remove the lock file if the --unlock option was specified if (((in_array($name, $projects_to_unlock)) || (in_array('all', $projects_to_unlock))) && (file_exists($lockfile))) { drush_op('unlink', $lockfile); } // Create the lock file if the --lock option was specified if ((in_array($name, $projects_to_lock)) || (in_array('all', $projects_to_lock))) { drush_op('file_put_contents', $lockfile, $lock_message != NULL ? $lock_message : "Locked via drush."); // Note that the project is locked. This will work even if we are simulated, // or if we get permission denied from the file_put_contents. // If the lock is -not- simulated or transient, then the lock message will be // read from the lock file below. $message = drush_get_context('DRUSH_SIMULATE') ? 'Simulated lock.' : 'Transient lock.'; } // If the persistent lock file exists, then mark the project as locked. if (file_exists($lockfile)) { $message = trim(file_get_contents($lockfile)); } } // If there is a message set, then mark the project as locked. if (isset($message)) { $projects[$name]['locked'] = !empty($message) ? $message : "Locked."; $locked_result[$name] = $project; } } return $locked_result; } /** * Returns the path to the extensions cache file. */ function _drush_pm_extension_cache_file() { return drush_get_context('DRUSH_PER_USER_CONFIGURATION') . "/drush-extension-cache.inc"; } /** * Load the extensions cache. */ function _drush_pm_get_extension_cache() { $extension_cache = array(); $cache_file = _drush_pm_extension_cache_file(); if (file_exists($cache_file)) { include $cache_file; } if (!array_key_exists('extension-map', $extension_cache)) { $extension_cache['extension-map'] = array(); } return $extension_cache; } /** * Lookup an extension in the extensions cache. */ function drush_pm_lookup_extension_in_cache($extension) { $result = NULL; $extension_cache = _drush_pm_get_extension_cache(); if (!empty($extension_cache) && array_key_exists($extension, $extension_cache)) { $result = $extension_cache[$extension]; } return $result; } /** * Persists extensions cache. * * #TODO# not implemented. */ function drush_pm_put_extension_cache($extension_cache) { } /** * Store extensions founds within a project in extensions cache. */ function drush_pm_cache_project_extensions($project, $found) { $extension_cache = _drush_pm_get_extension_cache(); foreach($found as $extension) { // Simple cache does not handle conflicts // We could keep an array of projects, and count // how many times each one has been seen... $extension_cache[$extension] = $project['name']; } drush_pm_put_extension_cache($extension_cache); } /** * Print out all extensions (modules/themes/profiles) found in specified project. * * Find .info.yml files in the project path and identify modules, themes and * profiles. It handles two kind of projects: drupal core/profiles and * modules/themes. * It does nothing with theme engine projects. */ function drush_pm_extensions_in_project($project) { // Mask for drush_scan_directory, to match .info.yml files. $mask = $project['drupal_version'][0] >= 8 ? '/(.*)\.info\.yml$/' : '/(.*)\.info$/'; // Mask for drush_scan_directory, to avoid tests directories. $nomask = array('.', '..', 'CVS', 'tests'); // Drupal core and profiles can contain modules, themes and profiles. if (in_array($project['project_type'], array('core', 'profile'))) { $found = array('profile' => array(), 'theme' => array(), 'module' => array()); // Find all of the .info files foreach (drush_scan_directory($project['full_project_path'], $mask, $nomask) as $filename => $info) { // Extract extension name from filename. $matches = array(); preg_match($mask, $info->basename, $matches); $name = $matches[1]; // Find the project type corresponding the .info file. // (Only drupal >=7.x has .info for .profile) $base = dirname($filename) . '/' . $name; if (is_file($base . '.module')) { $found['module'][] = $name; } else if (is_file($base . '.profile')) { $found['profile'][] = $name; } else { $found['theme'][] = $name; } } // Special case: find profiles for drupal < 7.x (no .info) if ($project['drupal_version'][0] < 7) { foreach (drush_find_profiles($project['full_project_path']) as $filename => $info) { $found['profile'][] = $info->name; } } // Log results. $msg = "Project !project contains:\n"; $args = array('!project' => $project['name']); foreach (array_keys($found) as $type) { if ($count = count($found[$type])) { $msg .= " - !count_$type !type_$type: !found_$type\n"; $args += array("!count_$type" => $count, "!type_$type" => $type, "!found_$type" => implode(', ', $found[$type])); if ($count > 1) { $args["!type_$type"] = $type.'s'; } } } drush_log(dt($msg, $args), LogLevel::SUCCESS); drush_print_pipe(call_user_func_array('array_merge', array_values($found))); } // Modules and themes can only contain other extensions of the same type. elseif (in_array($project['project_type'], array('module', 'theme'))) { $found = array(); foreach (drush_scan_directory($project['full_project_path'], $mask, $nomask) as $filename => $info) { // Extract extension name from filename. $matches = array(); preg_match($mask, $info->basename, $matches); $found[] = $matches[1]; } // If there is only one module / theme in the project, only print out // the message if is different than the project name. if (count($found) == 1) { if ($found[0] != $project['name']) { $msg = "Project !project contains a !type named !found."; } } // If there are multiple modules or themes in the project, list them all. else { $msg = "Project !project contains !count !types: !found."; } if (isset($msg)) { drush_print(dt($msg, array('!project' => $project['name'], '!count' => count($found), '!type' => $project['project_type'], '!found' => implode(', ', $found)))); } drush_print_pipe($found); // Cache results. drush_pm_cache_project_extensions($project, $found); } } /** * Return an array of empty directories. * * Walk a directory and return an array of subdirectories that are empty. Will * return the given directory if it's empty. * If a list of items to exclude is provided, subdirectories will be condidered * empty even if they include any of the items in the list. * * @param string $dir * Path to the directory to work in. * @param array $exclude * Array of files or directory to exclude in the check. * * @return array * A list of directory paths that are empty. A directory is deemed to be empty * if it only contains excluded files or directories. */ function drush_find_empty_directories($dir, $exclude = array()) { // Skip files. if (!is_dir($dir)) { return array(); } $to_exclude = array_merge(array('.', '..'), $exclude); $empty_dirs = array(); $dir_is_empty = TRUE; foreach (scandir($dir) as $file) { // Skip excluded directories. if (in_array($file, $to_exclude)) { continue; } // Recurse into sub-directories to find potentially empty ones. $subdir = $dir . '/' . $file; $empty_dirs += drush_find_empty_directories($subdir, $exclude); // $empty_dir will not contain $subdir, if it is a file or if the // sub-directory is not empty. $subdir is only set if it is empty. if (!isset($empty_dirs[$subdir])) { $dir_is_empty = FALSE; } } if ($dir_is_empty) { $empty_dirs[$dir] = $dir; } return $empty_dirs; } /** * Inject metadata into all .info files for a given project. * * @param string $project_dir * The full path to the root directory of the project to operate on. * @param string $project_name * The project machine name (AKA shortname). * @param string $version * The version string to inject into the .info file(s). * @param int $datestamp * The datestamp of the last commit. * * @return boolean * TRUE on success, FALSE on any failures appending data to .info files. */ function drush_pm_inject_info_file_metadata($project_dir, $project_name, $version, $datestamp) { // `drush_drupal_major_version()` cannot be used here because this may be running // outside of a Drupal context. $yaml_format = substr($version, 0, 1) >= 8; $pattern = preg_quote($yaml_format ? '.info.yml' : '.info'); $info_files = drush_scan_directory($project_dir, '/.*' . $pattern . '$/'); if (!empty($info_files)) { // Construct the string of metadata to append to all the .info files. if ($yaml_format) { $info = _drush_pm_generate_info_yaml_metadata($version, $project_name, $datestamp); } else { $info = _drush_pm_generate_info_ini_metadata($version, $project_name, $datestamp); } foreach ($info_files as $info_file) { if (!drush_file_append_data($info_file->filename, $info)) { return FALSE; } } } return TRUE; } /** * Generate version information for `.info` files in ini format. * * Taken with some modifications from: * http://drupalcode.org/project/drupalorg.git/blob/refs/heads/6.x-3.x:/drupalorg_project/plugins/release_packager/DrupalorgProjectPackageRelease.class.php#l192 */ function _drush_pm_generate_info_ini_metadata($version, $project_name, $datestamp) { $matches = array(); $extra = ''; if (preg_match('/^((\d+)\.x)-.*/', $version, $matches) && $matches[2] >= 6) { $extra .= "\ncore = \"$matches[1]\""; } if (!drush_get_option('no-gitprojectinfo', FALSE)) { $extra = "\nproject = \"$project_name\""; } $date = date('Y-m-d', $datestamp); $info = <<= 6) { $extra .= "\ncore: '$matches[1]'"; } if (!drush_get_option('no-gitprojectinfo', FALSE)) { $extra = "\nproject: '$project_name'"; } $date = date('Y-m-d', $datestamp); $info = << $status))); } } } /** * Implementation of drush_hook_COMMAND(). */ function drush_pm_projectinfo() { // Get specific requests. $requests = pm_parse_arguments(func_get_args(), FALSE); // Get installed extensions and projects. $extensions = drush_get_extensions(); $projects = drush_get_projects($extensions); // If user did not specify any projects, return them all if (empty($requests)) { $result = $projects; } else { $result = array(); foreach ($requests as $name) { if (array_key_exists($name, $projects)) { $result[$name] = $projects[$name]; } else { drush_log(dt('!project was not found.', array('!project' => $name)), LogLevel::WARNING); continue; } } } // Find the Drush commands that belong with each project. foreach ($result as $name => $project) { $drush_commands = pm_projectinfo_commands_in_project($project); if (!empty($drush_commands)) { $result[$name]['drush'] = $drush_commands; } } // If user specified --drush, remove projects with no drush extensions if (drush_get_option('drush')) { foreach ($result as $name => $project) { if (!array_key_exists('drush', $project)) { unset($result[$name]); } } } // If user specified --status=1|0, remove projects with a distinct status. if (($status = drush_get_option('status', FALSE)) !== FALSE) { $status_code = ($status == 'enabled') ? 1 : 0; foreach ($result as $name => $project) { if ($project['status'] != $status_code) { unset($result[$name]); } } } return $result; } function pm_projectinfo_commands_in_project($project) { $drush_commands = array(); if (array_key_exists('path', $project)) { $commands = drush_get_commands(); foreach ($commands as $commandname => $command) { if (!array_key_exists("is_alias", $command) && ($command['path'] == $project['path'])) { $drush_commands[] = $commandname; } } } return $drush_commands; } FALSE, ); $values = drush_invoke_process("@self", 'pm-updatestatus', func_get_args(), $updatestatus_options, $backend_options); if (!is_array($values) || $values['error_status']) { return drush_set_error('pm-updatestatus failed.'); } $last = $update_status->lastCheck(); drush_print(dt('Update information last refreshed: ') . ($last ? format_date($last) : dt('Never'))); drush_print($values['output']); $update_info = $values['object']; // Prevent update of core if --no-core was specified. if (isset($update_info['drupal']) && drush_get_option('no-core', FALSE)) { unset($update_info['drupal']); drush_print(dt('Skipping core update (--no-core specified).')); } // Remove locked and non-updateable projects. foreach ($update_info as $name => $project) { if ((isset($project['locked']) && !isset($requests[$name])) || (!isset($project['updateable']) || !$project['updateable'])) { unset($update_info[$name]); } } // Do no updates in simulated mode. if (drush_get_context('DRUSH_SIMULATE')) { return drush_log(dt('No action taken in simulated mode.'), LogLevel::OK); return TRUE; } $tmpfile = drush_tempnam('pm-updatecode.'); $core_update_available = FALSE; if (isset($update_info['drupal'])) { $drupal_project = $update_info['drupal']; unset($update_info['drupal']); // At present we need to update drupal core after non-core projects // are updated. if (empty($update_info)) { return _pm_update_core($drupal_project, $tmpfile); } // If there are modules other than drupal core enabled, then update them // first. else { $core_update_available = TRUE; if ($drupal_project['status'] == DRUSH_UPDATESTATUS_NOT_SECURE) { drush_print(dt("NOTE: A security update for the Drupal core is available.")); } else { drush_print(dt("NOTE: A code update for the Drupal core is available.")); } drush_print(dt("Drupal core will be updated after all of the non-core projects are updated.\n")); } } // If there are no releases to update, then print a final // exit message. if (empty($update_info)) { if (drush_get_option('security-only')) { return drush_log(dt('No security updates available.'), LogLevel::OK); } else { return drush_log(dt('No code updates available.'), LogLevel::OK); } } // Offer to update to the identified releases. if (!pm_update_packages($update_info, $tmpfile)) { return FALSE; } // After projects are updated we can update core. if ($core_update_available) { drush_print(); return _pm_update_core($drupal_project, $tmpfile); } } /** * Update drupal core, following interactive confirmation from the user. * * @param $project * The drupal project information from the drupal.org update service, * copied from $update_info['drupal']. @see drush_pm_updatecode. * * @return bool * Success or failure. An error message will be logged. */ function _pm_update_core(&$project, $tmpfile) { $release_info = drush_get_engine('release_info'); drush_print(dt('Code updates will be made to drupal core.')); drush_print(dt("WARNING: Updating core will discard any modifications made to Drupal core files, most noteworthy among these are .htaccess and robots.txt. If you have made any modifications to these files, please back them up before updating so that you can re-create your modifications in the updated version of the file.")); drush_print(dt("Note: Updating core can potentially break your site. It is NOT recommended to update production sites without prior testing.")); drush_print(); if (drush_get_option('notes', FALSE)) { drush_print('Obtaining release notes for above projects...'); #TODO# Build the $request array from info in $project. $request = pm_parse_request('drupal'); $release_info->get($request)->getReleaseNotes(NULL, TRUE, $tmpfile); } if(!drush_confirm(dt('Do you really want to continue?'))) { drush_print(dt('Rolling back all changes. Run again with --no-core to update modules only.')); return drush_user_abort(); } $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT'); // We need write permission on $drupal_root. if (!is_writable($drupal_root)) { return drush_set_error('DRUSH_PATH_NO_WRITABLE', dt('Drupal root path is not writable.')); } // Create a directory 'core' if it does not already exist. $project['path'] = 'drupal-' . $project['candidate_version']; $project['full_project_path'] = $drupal_root . '/' . $project['path']; if (!is_dir($project['full_project_path'])) { drush_mkdir($project['full_project_path']); } // Create a list of directories to exclude from the update process. // On Drupal >=8 skip also directories in the document root. if (drush_drupal_major_version() >= 8) { $skip_list = array('sites', $project['path'], 'modules', 'profiles', 'themes'); } else { $skip_list = array('sites', $project['path']); } // Add non-writable directories: we can't move them around. // We will also use $items_to_test later for $version_control check. $items_to_test = drush_scan_directory($drupal_root, '/.*/', array_merge(array('.', '..'), $skip_list), 0, FALSE, 'basename', 0, TRUE); foreach (array_keys($items_to_test) as $item) { if (is_dir($item) && !is_writable($item)) { $skip_list[] = $item; unset($items_to_test[$item]); } elseif (is_link($item)) { $skip_list[] = $item; unset($items_to_test[$item]); } } $project['skip_list'] = $skip_list; // Move all files and folders in $drupal_root to the new 'core' directory // except for the items in the skip list _pm_update_move_files($drupal_root, $project['full_project_path'], $project['skip_list']); // Set a context variable to indicate that rollback should reverse // the _pm_update_move_files above. drush_set_context('DRUSH_PM_DRUPAL_CORE', $project); if (!$version_control = drush_pm_include_version_control($project['full_project_path'])) { return FALSE; } // Check we have a version control system, and it clears its pre-flight. if (!$version_control->pre_update($project, $items_to_test)) { return FALSE; } // Update core. if (pm_update_project($project, $version_control) === FALSE) { return FALSE; } // Take the updated files in the 'core' directory that have been updated, // and move all except for the items in the skip list back to // the drupal root. _pm_update_move_files($project['full_project_path'], $drupal_root, $project['skip_list']); drush_delete_dir($project['full_project_path']); $project['full_project_path'] = $drupal_root; // If there is a backup target, then find items // in the backup target that do not exist at the // drupal root. These are to be moved back. if (array_key_exists('backup_target', $project)) { _pm_update_move_files($project['backup_target'], $drupal_root, $project['skip_list'], FALSE); _pm_update_move_files($project['backup_target'] . '/profiles', $drupal_root . '/profiles', array('default'), FALSE); } pm_update_finish($project, $version_control); return TRUE; } /** * Move some files from one location to another. */ function _pm_update_move_files($src_dir, $dest_dir, $skip_list, $remove_conflicts = TRUE) { $items_to_move = drush_scan_directory($src_dir, '/.*/', array_merge(array('.', '..'), $skip_list), 0, FALSE, 'filename', 0, TRUE); foreach ($items_to_move as $filename => $info) { if ($remove_conflicts) { drush_delete_dir($dest_dir . '/' . basename($filename)); } if (!file_exists($dest_dir . '/' . basename($filename))) { $move_result = drush_move_dir($filename, $dest_dir . '/' . basename($filename)); } } return TRUE; } /** * Update projects according to an array of releases and print the release notes * for each project, following interactive confirmation from the user. * * @param $update_info * An array of projects from the drupal.org update service, with an additional * array key candidate_version that specifies the version to be installed. */ function pm_update_packages($update_info, $tmpfile) { $release_info = drush_get_engine('release_info'); $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT'); $print = ''; $status = array(); foreach($update_info as $project) { $print .= $project['title'] . " [" . $project['name'] . '-' . $project['candidate_version'] . "], "; $status[$project['status']] = $project['status']; } // We print the list of the projects that need to be updated. if (isset($status[DRUSH_UPDATESTATUS_NOT_SECURE])) { if (isset($status[DRUSH_UPDATESTATUS_NOT_CURRENT])) { $title = (dt('Security and code updates will be made to the following projects:')); } else { $title = (dt('Security updates will be made to the following projects:')); } } else { $title = (dt('Code updates will be made to the following projects:')); } $print = "$title " . (substr($print, 0, strlen($print)-2)); drush_print($print); file_put_contents($tmpfile, "\n\n$print\n\n", FILE_APPEND); // Print the release notes for projects to be updated. if (drush_get_option('notes', FALSE)) { drush_print('Obtaining release notes for above projects...'); #TODO# Build the $request array from info in $project. foreach (array_keys($update_info) as $project_name) { $request = pm_parse_request($project_name); $release_info->get($request)->getReleaseNotes(NULL, TRUE, $tmpfile); } } // We print some warnings before the user confirms the update. drush_print(); if (drush_get_option('no-backup', FALSE)) { drush_print(dt("Note: You have selected to not store backups.")); } else { drush_print(dt("Note: A backup of your project will be stored to backups directory if it is not managed by a supported version control system.")); drush_print(dt('Note: If you have made any modifications to any file that belongs to one of these projects, you will have to migrate those modifications after updating.')); } if(!drush_confirm(dt('Do you really want to continue with the update process?'))) { return drush_user_abort(); } // Now we start the actual updating. foreach($update_info as $project) { if (empty($project['path'])) { return drush_set_error('DRUSH_PM_UPDATING_NO_PROJECT_PATH', dt('The !project project path is not available, perhaps the !type is enabled but has been deleted from disk.', array('!project' => $project['name'], '!type' => $project['project_type']))); } drush_log(dt('Starting to update !project code at !dir...', array('!project' => $project['title'], '!dir' => $project['path']))); // Define and check the full path to project directory and base (parent) directory. $project['full_project_path'] = $drupal_root . '/' . $project['path']; if (stripos($project['path'], $project['project_type']) === FALSE || !is_dir($project['full_project_path'])) { return drush_set_error('DRUSH_PM_UPDATING_PATH_NOT_FOUND', dt('The !project directory could not be found within the !types directory at !full_project_path, perhaps the project is enabled but has been deleted from disk.', array('!project' => $project['name'], '!type' => $project['project_type'], '!full_project_path' => $project['full_project_path']))); } if (!$version_control = drush_pm_include_version_control($project['full_project_path'])) { return FALSE; } // Check we have a version control system, and it clears its pre-flight. if (!$version_control->pre_update($project)) { return FALSE; } // Run update on one project. if (pm_update_project($project, $version_control) === FALSE) { return FALSE; } pm_update_finish($project, $version_control); } return TRUE; } /** * Update one project -- a module, theme or Drupal core. * * @param $project * The project to upgrade. $project['full_project_path'] must be set * to the location where this project is stored. * @return bool * Success or failure. An error message will be logged. */ function pm_update_project($project, $version_control) { // 1. If the version control engine is a proper vcs we need to remove project // files in order to not have orphan files after update. // 2. If the package-handler is cvs or git, it will remove upstream removed // files and no orphans will exist after update. // So, we must remove all files previous update if the directory is not a // working copy of cvs or git but we don't need to remove them if the version // control engine is backup, as it did already move the project out to the // backup directory. if (($version_control->engine != 'backup') && (drush_get_option('package-handler', 'wget') == 'wget')) { // Find and unlink all files but the ones in the vcs control directories. $skip_list = array('.', '..'); $skip_list = array_merge($skip_list, drush_version_control_reserved_files()); drush_scan_directory($project['full_project_path'], '/.*/', $skip_list, 'unlink', TRUE, 'filename', 0, TRUE); } // Add the project to a context so we can roll back if needed. $updated = drush_get_context('DRUSH_PM_UPDATED'); $updated[] = $project; drush_set_context('DRUSH_PM_UPDATED', $updated); if (!package_handler_update_project($project, $project['releases'][$project['candidate_version']])) { return drush_set_error('DRUSH_PM_UPDATING_FAILED', dt('Updating project !project failed. Attempting to roll back to previously installed version.', array('!project' => $project['name']))); } // If the version control engine is a proper vcs we also need to remove // orphan directories. if (($version_control->engine != 'backup') && (drush_get_option('package-handler', 'wget') == 'wget')) { $files = drush_find_empty_directories($project['full_project_path'], $version_control->reserved_files()); array_map('drush_delete_dir', $files); } return TRUE; } /** * Run the post-update hooks after updatecode is finished for one project. */ function pm_update_finish($project, $version_control) { drush_print(dt('Project !project was updated successfully. Installed version is now !version.', array('!project' => $project['name'], '!version' => $project['candidate_version']))); drush_command_invoke_all('pm_post_update', $project['name'], $project['releases'][$project['candidate_version']], $project); $version_control->post_update($project); } /** * Rollback the update process. */ function drush_pm_updatecode_rollback() { $projects = array_reverse(drush_get_context('DRUSH_PM_UPDATED', array())); foreach($projects as $project) { drush_log(dt('Rolling back update of !project code ...', array('!project' => $project['title']))); // Check we have a version control system, and it clears it's pre-flight. if (!$version_control = drush_pm_include_version_control($project['path'])) { return FALSE; } $version_control->rollback($project); } // Post rollback, we will do additional repair if the project is drupal core. $drupal_core = drush_get_context('DRUSH_PM_DRUPAL_CORE', FALSE); if ($drupal_core) { $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT'); _pm_update_move_files($drupal_core['full_project_path'], $drupal_root, $drupal_core['skip_list']); drush_delete_dir($drupal_core['full_project_path']); } } engine == 'drupal') ? NULL : FALSE; $check_disabled = drush_get_option('check-disabled', $check_disabled_default); $update_info = $update_status->getStatus($projects, $check_disabled); foreach ($extensions as $name => $extension) { // Add an item to $update_info for each enabled extension which was obtained // from cvs or git and its project is unknown (because of cvs_deploy or // git_deploy is not enabled). if (!isset($extension->info['project'])) { if ((isset($extension->vcs)) && ($extension->status)) { $update_info[$name] = array( 'name' => $name, 'label' => $extension->label, 'existing_version' => 'Unknown', 'status' => DRUSH_UPDATESTATUS_PROJECT_NOT_PACKAGED, 'status_msg' => dt('Project was not packaged by drupal.org but obtained from !vcs. You need to enable !vcs_deploy module', array('!vcs' => $extension->vcs)), ); // The user may have requested to update a project matching this // extension. If it was by coincidence or error we don't mind as we've // already added an item to $update_info. Just clean up $requests. if (isset($requests[$name])) { unset($requests[$name]); } } } // Additionally if the extension name is distinct to the project name and // the user asked to update the extension, fix the request. elseif ((isset($requests[$name])) && ($name != $extension->info['project'])) { $requests[$extension->info['project']] = $requests[$name]; unset($requests[$name]); } } // If specific project updates were requested then remove releases for all // others. $requested = func_get_args(); if (!empty($requested)) { foreach ($update_info as $name => $project) { if (!isset($requests[$name])) { unset($update_info[$name]); } } } // Add an item to $update_info for each request not present in $update_info. foreach ($requests as $name => $request) { if (!isset($update_info[$name])) { // Disabled projects. if ((isset($projects[$name])) && ($projects[$name]['status'] == 0)) { $update_info[$name] = array( 'name' => $name, 'label' => $projects[$name]['label'], 'existing_version' => $projects[$name]['version'], 'status' => DRUSH_UPDATESTATUS_REQUESTED_PROJECT_NOT_UPDATEABLE, ); unset($requests[$name]); } // At this point we are unable to find matching installed project. // It does not exist at all or it is misspelled,... else { $update_info[$name] = array( 'name' => $name, 'label' => $name, 'existing_version' => 'Unknown', 'status'=> DRUSH_UPDATESTATUS_REQUESTED_PROJECT_NOT_FOUND, ); } } } // If specific versions were requested, match the requested release. foreach ($requests as $name => $request) { if (!empty($request['version'])) { if (empty($update_info[$name]['releases'][$request['version']])) { $update_info[$name]['status'] = DRUSH_UPDATESTATUS_REQUESTED_VERSION_NOT_FOUND; } elseif ($request['version'] == $update_info[$name]['existing_version']) { $update_info[$name]['status'] = DRUSH_UPDATESTATUS_REQUESTED_VERSION_CURRENT; } // TODO: should we warn/reject if this is a downgrade? else { $update_info[$name]['status'] = DRUSH_UPDATESTATUS_REQUESTED_VERSION_NOT_CURRENT; $update_info[$name]['candidate_version'] = $request['version']; } } } // Process locks specified on the command line. $locked_list = drush_pm_update_lock($update_info, drush_get_option_list('lock'), drush_get_option_list('unlock'), drush_get_option('lock-message')); // Build project updatable messages, set candidate version and mark // 'updateable' in the project. foreach ($update_info as $key => $project) { switch($project['status']) { case DRUSH_UPDATESTATUS_NOT_SECURE: $status = dt('SECURITY UPDATE available'); pm_release_recommended($project); break; case DRUSH_UPDATESTATUS_REVOKED: $status = dt('Installed version REVOKED'); pm_release_recommended($project); break; case DRUSH_UPDATESTATUS_NOT_SUPPORTED: $status = dt('Installed version not supported'); pm_release_recommended($project); break; case DRUSH_UPDATESTATUS_NOT_CURRENT: $status = dt('Update available'); pm_release_recommended($project); break; case DRUSH_UPDATESTATUS_CURRENT: $status = dt('Up to date'); pm_release_recommended($project); $project['updateable'] = FALSE; break; case DRUSH_UPDATESTATUS_NOT_CHECKED: case DRUSH_UPDATESTATUS_NOT_FETCHED: case DRUSH_UPDATESTATUS_FETCH_PENDING: $status = dt('Unable to check status'); break; case DRUSH_UPDATESTATUS_PROJECT_NOT_PACKAGED: $status = $project['status_msg']; break; case DRUSH_UPDATESTATUS_REQUESTED_PROJECT_NOT_UPDATEABLE: $status = dt('Project has no enabled extensions and can\'t be updated'); break; case DRUSH_UPDATESTATUS_REQUESTED_PROJECT_NOT_FOUND: $status = dt('Specified project not found'); break; case DRUSH_UPDATESTATUS_REQUESTED_VERSION_NOT_FOUND: $status = dt('Specified version not found'); break; case DRUSH_UPDATESTATUS_REQUESTED_VERSION_CURRENT: $status = dt('Specified version already installed'); break; case DRUSH_UPDATESTATUS_REQUESTED_VERSION_NOT_CURRENT: $status = dt('Specified version available'); $project['updateable'] = TRUE; break; default: $status = dt('Unknown'); break; } if (isset($project['locked'])) { $status = $project['locked'] . " ($status)"; } // Persist candidate_version in $update_info (plural). if (empty($project['candidate_version'])) { $update_info[$key]['candidate_version'] = $project['existing_version']; // Default to no change } else { $update_info[$key]['candidate_version'] = $project['candidate_version']; } $update_info[$key]['status_msg'] = $status; if (isset($project['updateable'])) { $update_info[$key]['updateable'] = $project['updateable']; } } // Filter projects to show. return pm_project_filter($update_info, drush_get_option('security-only')); } /** * Filter projects based on verbosity level and $security_only flag. * * @param array $update_info * Update info for projects. * @param bool $security_only * Whether to select only projects with security updates. * * @return * Array of projects matching filter criteria. */ function pm_project_filter($update_info, $security_only) { $eligible = array(); foreach ($update_info as $key => $project) { if ($security_only) { if ($project['status'] == DRUSH_UPDATESTATUS_NOT_SECURE) { $eligible[$key] = $project; } } elseif (drush_get_context('DRUSH_VERBOSE')) { $eligible[$key] = $project; } elseif ($project['status'] != DRUSH_UPDATESTATUS_CURRENT) { $eligible[$key] = $project; } } return $eligible; } /** * Set a release to a recommended version (if available), and set as updateable. */ function pm_release_recommended(&$project) { if (isset($project['recommended'])) { $project['candidate_version'] = $project['recommended']; $project['updateable'] = TRUE; } // If installed version is dev and the candidate version is older, choose // latest dev as candidate. if (($project['install_type'] == 'dev') && isset($project['candidate_version'])) { if ($project['releases'][$project['candidate_version']]['date'] < $project['datestamp']) { $project['candidate_version'] = $project['latest_dev']; if ($project['releases'][$project['candidate_version']]['date'] <= $project['datestamp']) { $project['candidate_version'] = $project['existing_version']; $project['updateable'] = FALSE; } } } } prepare_backup_dir()) { if ($project['project_type'] != 'core') { $backup_target .= '/' . $project['project_type'] . 's'; drush_mkdir($backup_target); } $backup_target .= '/'. $project['name']; // Save for rollback or notifications. $project['backup_target'] = $backup_target; // Move or copy to backup target based in package-handler. if (drush_get_option('package-handler', 'wget') == 'wget') { if (drush_move_dir($project['full_project_path'], $backup_target)) { return TRUE; } } // cvs or git. elseif (drush_copy_dir($project['full_project_path'], $backup_target)) { return TRUE; } return drush_set_error('DRUSH_PM_BACKUP_FAILED', dt('Failed to backup project directory !project to !backup_target', array('!project' => $project['full_project_path'], '!backup_target' => $backup_target))); } } /** * Implementation of rollback(). */ public function rollback($project) { if (drush_get_option('no-backup', FALSE)) { return; } if (drush_move_dir($project['backup_target'], $project['full_project_path'], TRUE)) { return drush_log(dt("Backups were restored successfully."), LogLevel::OK); } return drush_set_error('DRUSH_PM_BACKUP_ROLLBACK_FAILED', dt('Could not restore backup and rollback from failed upgrade. You will need to resolve manually.')); } /** * Implementation of post_update(). */ public function post_update($project) { if (drush_get_option('no-backup', FALSE)) { return; } if ($project['backup_target']) { drush_log(dt("Backups were saved into the directory !backup_target.", array('!backup_target' => $project['backup_target'])), LogLevel::OK); } } /** * Implementation of post_download(). */ public function post_download($project) { // NOOP } // Helper for pre_update. public function prepare_backup_dir($subdir = NULL) { return drush_prepare_backup_dir($subdir); } public static function reserved_files() { return array(); } } '.'); } $args = array_keys($items_to_test); array_unshift($args, 'bzr status --short' . str_repeat(' %s', count($args))); array_unshift($args, $project['full_project_path']); if (call_user_func_array('drush_shell_cd_and_exec', $args)) { $output = preg_grep('/^[\sRCP][\sNDKM][\s\*]/', drush_shell_exec_output()); if (!empty($output)) { return drush_set_error('DRUSH_PM_BZR_LOCAL_CHANGES', dt("The Bazaar working copy at !path appears to have uncommitted changes (see below). Please commit or revert these changes before continuing:\n!output", array('!path' => $project['full_project_path'], '!output' => implode("\n", $output)))); } } else { return drush_set_error('DRUSH_PM_BZR_NOT_FOUND', dt("Drush was unable to get the bzr status on !path. Check that you have Bazaar \ninstalled and that this directory is a Bazaar working copy.\nThe specific errors are below:\n!errors", array('!path' => $project['full_project_path'], '!errors' => implode("\n", drush_shell_exec_output())))); } return TRUE; } /** * Implementation of rollback(). */ public function rollback($project) { if (drush_shell_exec('bzr revert %s', $project['full_project_path'])) { $output = drush_shell_exec_output(); if (!empty($output)) { return drush_set_error('DRUSH_PM_BZR_LOCAL_CHANGES', dt("The Bazaar working copy at !path appears to have uncommitted changes (see below). Please commit or revert these changes before continuing:\n!output", array('!path' => $project['full_project_path'], '!output' => implode("\n", $output)))); } } else { return drush_set_error('DRUSH_PM_BZR_NOT_FOUND', dt("Drush was unable to get the Bazaar status on !path. Check that you have Bazaar \ninstalled and that this directory is a Bazaar working copy.\nThe specific errors are below:\n!errors", array('!path' => $project['full_project_path'], '!errors' => implode("\n", drush_shell_exec_output())))); } } /** * Implementation of post_update(). */ public function post_update($project) { if ($this->sync($project)) { // Only attempt commit on a sucessful sync $this->commit($project); } } /** * Implementation of post_download(). */ public function post_download($project) { if ($this->sync($project)) { // Only attempt commit on a sucessful sync $this->commit($project); } } /** * Automatically add any unversioned files to Bazaar and remove any files * that have been deleted on the file system */ private function sync($project) { if (drush_get_option('bzrsync')) { $errors = ''; $root = array(); if (drush_shell_exec('bzr status --short %s', $project['full_project_path'])) { $output = drush_shell_exec_output(); // All paths returned by bzr status are relative to the repository root. if (drush_shell_exec('bzr root %s', $project['full_project_path'])) { $root = drush_shell_exec_output(); } foreach ($output as $line) { if (preg_match('/^\?\s+(.*)/', $line, $matches)) { $path = $root[0] .'/'. $matches[1]; if (!drush_shell_exec('bzr add --no-recurse %s', $path)) { $errors .= implode("\n", drush_shell_exec_output()); } } else if (preg_match('/^\s+D\s+(.*)/', $line, $matches)) { $path = $root[0] .'/'. $matches[1]; if (!drush_shell_exec('bzr remove %s', $path)) { $errors .= implode("\n", drush_shell_exec_output()); } } } if (!empty($errors)) { return drush_set_error('DRUSH_PM_BZR_SYNC_PROBLEMS', dt("Problems were encountered adding or removing files to/from Bazaar.\nThe specific errors are below:\n!errors", array('!errors' => $errors))); } } else { return drush_set_error('DRUSH_PM_BZR_NOT_FOUND', dt("Drush was unable to get the bzr status. Check that you have Bazaar \ninstalled and that the site is a Bazaar working copy.\nThe specific errors are below:\n!errors", array('!errors' => implode("\n", drush_shell_exec_output())))); } return TRUE; } } /** * Automatically commit changes to the repository */ private function commit($project) { if (drush_get_option('bzrcommit')) { $message = drush_get_option('bzrmessage'); if (empty($message)) { $message = dt("Drush automatic commit.\nProject: @name @type\nCommand: @arguments", array('@name' => $project['name'], '@type' => $project['project_type'], '@arguments' => implode(' ', $_SERVER['argv']))); } if (drush_shell_exec('bzr commit --message=%s %s', $message, $project['full_project_path'])) { drush_log(dt('Project committed to Bazaar successfully'), LogLevel::OK); } else { drush_set_error('DRUSH_PM_BZR_COMMIT_PROBLEMS', dt("Problems were encountered committing your changes to Bazaar.\nThe specific errors are below:\n!errors", array('!errors' => implode("\n", drush_shell_exec_output())))); } } else { drush_print(dt("You should consider committing the new code to your Bazaar repository.\nIf this version becomes undesireable, use Bazaar to roll back.")); } } public static function reserved_files() { return array('.bzr', '.bzrignore', '.bzrtags'); } } $project['full_project_path'], '!output' => implode("\n", $output)))); } } else { return drush_set_error('DRUSH_PM_SVN_NOT_FOUND', dt("Drush was unable to get the svn status on !path.\nThe specific errors are below:\n!errors", array('!path' => $project['full_project_path'], '!errors' => implode("\n", drush_shell_exec_output())))); } // Check for incoming updates $args = array_keys($items_to_test); array_unshift($args, 'svn status -u '. drush_get_option('svnstatusparams') . str_repeat('%s ', count($args))); array_unshift($args, $project['full_project_path']); if (call_user_func_array('drush_shell_cd_and_exec', $args)) { $output = preg_grep('/\*/', drush_shell_exec_output()); if (!empty($output)) { return drush_set_error('DRUSH_PM_SVN_REMOTE_CHANGES', dt("The SVN working copy at !path appears to be out of date with the repository (see below). Please run 'svn update' to pull down changes before continuing:\n!output", array('!path' => $project['full_project_path'], '!output' => implode("\n", $output)))); } } else { return drush_set_error('DRUSH_PM_SVN_NOT_FOUND', dt("Drush was unable to get the svn remote status on !path. Check that you have connectivity to the repository.\nThe specific errors are below:\n!errors", array('!path' => $project['full_project_path'], '!errors' => implode("\n", drush_shell_exec_output())))); } return TRUE; } /** * Implementation of rollback(). */ public function rollback($project) { if (drush_shell_exec('svn revert '. drush_get_option('svnrevertparams') .' '. $project['full_project_path'])) { $output = drush_shell_exec_output(); if (!empty($output)) { return drush_set_error('DRUSH_PM_SVN_LOCAL_CHANGES', dt("The SVN working copy at !path appears to have uncommitted changes (see below). Please commit or revert these changes before continuing:\n!output", array('!path' => $project['full_project_path'], '!output' => implode("\n", $output)))); } } else { return drush_set_error('DRUSH_PM_SVN_NOT_FOUND', dt("Drush was unable to get the svn status on !path. Check that you have Subversion \ninstalled and that this directory is a subversion working copy.\nThe specific errors are below:\n!errors", array('!path' => $project['full_project_path'], '!errors' => implode("\n", drush_shell_exec_output())))); } } /** * Implementation of post_update(). */ public function post_update($project) { if ($this->sync($project)) { // Only attempt commit on a sucessful sync $this->commit($project); } } /** * Implementation of post_download(). */ public function post_download($project) { if ($this->sync($project)) { // Only attempt commit on a sucessful sync $this->commit($project); } } /** * Automatically add any unversioned files to Subversion and remove any files * that have been deleted on the file system */ private function sync($project) { if (drush_get_option('svnsync')) { $errors = ''; if (drush_shell_exec('svn status '. drush_get_option('svnstatusparams') .' '. $project['full_project_path'])) { $output = drush_shell_exec_output(); foreach ($output as $line) { if (preg_match('/^\? *(.*)/', $line, $matches)) { if (!drush_shell_exec('svn add '. drush_get_option('svnaddparams') .' '. $matches[1])) { $errors .= implode("\n", drush_shell_exec_output()); } } if (preg_match('/^\! *(.*)/', $line, $matches)) { if (!drush_shell_exec('svn remove '. drush_get_option('svnremoveparams') .' '. $matches[1])) { $errors .= implode("\n", drush_shell_exec_output()); } } } if (!empty($errors)) { return drush_set_error('DRUSH_PM_SVN_SYNC_PROBLEMS', dt("Problems were encountered adding or removing files to/from this SVN working copy.\nThe specific errors are below:\n!errors", array('!errors' => $errors))); } } else { return drush_set_error('DRUSH_PM_SVN_NOT_FOUND', dt("Drush was unable to get the svn status on !path. Check that you have Subversion \ninstalled and that this directory is a subversion working copy.\nThe specific errors are below:\n!errors", array('!path' => $project['full_project_path'], '!errors' => implode("\n", drush_shell_exec_output())))); } return TRUE; } } /** * Automatically commit changes to the repository */ private function commit($project) { if (drush_get_option('svncommit')) { $message = drush_get_option('svnmessage'); if (empty($message)) { $message = dt("Drush automatic commit: \n") . implode(' ', $_SERVER['argv']); } if (drush_shell_exec('svn commit '. drush_get_option('svncommitparams') .' -m "'. $message .'" '. $project['full_project_path'])) { drush_log(dt('Project committed to Subversion successfully'), LogLevel::OK); } else { drush_set_error('DRUSH_PM_SVN_COMMIT_PROBLEMS', dt("Problems were encountered committing your changes to Subversion.\nThe specific errors are below:\n!errors", array('!errors' => implode("\n", drush_shell_exec_output())))); } } else { drush_print(dt("You should consider committing the new code to your Subversion repository.\nIf this version becomes undesireable, use Subversion to roll back.")); } } public static function reserved_files() { return array('.svn'); } } uid; $message = strtr('Watchdog: !message | severity: !severity | type: !type | uid: !uid | !ip | !request_uri | !referer | !link', array( '!message' => strip_tags(!isset($log_entry['variables']) ? $log_entry['message'] : strtr($log_entry['message'], $log_entry['variables'])), '!severity' => $log_entry['severity'], '!type' => $log_entry['type'], '!ip' => $log_entry['ip'], '!request_uri' => $log_entry['request_uri'], '!referer' => $log_entry['referer'], '!uid' => $uid, '!link' => strip_tags($log_entry['link']), )); error_log($message); } } // Get a $_SERVER key, or equivalent environment variable // if it is not set in $_SERVER. function runserver_env($key) { if (isset($_SERVER[$key])) { return $_SERVER[$key]; } else { return getenv($key); } } $url = parse_url($_SERVER["REQUEST_URI"]); if (file_exists('.' . $url['path'])) { // Serve the requested resource as-is. return FALSE; } // Populate the "q" query key with the path, skip the leading slash. $_GET['q'] = $_REQUEST['q'] = substr($url['path'], 1); // We set the base_url so that Drupal generates correct URLs for runserver // (e.g. http://127.0.0.1:8888/...), but can still select and serve a specific // site in a multisite configuration (e.g. http://mysite.com/...). $base_url = runserver_env('RUNSERVER_BASE_URL'); // The built in webserver incorrectly sets $_SERVER['SCRIPT_NAME'] when URLs // contain multiple dots (such as config entity IDs) in the path. Since this is // a virtual resource, served by index.php set the script name explicitly. // See https://github.com/drush-ops/drush/issues/2033 for more information. $_SERVER['SCRIPT_NAME'] = '/index.php'; // Include the main index.php and let Drupal take over. // n.b. Drush sets the cwd to the Drupal root during bootstrap. include 'index.php'; id(); $message = strtr('Watchdog: !message | severity: !severity | type: !type | uid: !uid | !ip | !request_uri | !referer | !link', array( '!message' => strip_tags(!isset($log_entry['variables']) ? $log_entry['message'] : strtr($log_entry['message'], $log_entry['variables'])), '!severity' => $log_entry['severity'], '!type' => $log_entry['type'], '!ip' => $log_entry['ip'], '!request_uri' => $log_entry['request_uri'], '!referer' => $log_entry['referer'], '!uid' => $uid, '!link' => strip_tags($log_entry['link']), )); error_log($message); } } // Get a $_SERVER key, or equivalent environment variable // if it is not set in $_SERVER. function runserver_env($key) { if (isset($_SERVER[$key])) { return $_SERVER[$key]; } else { return getenv($key); } } $url = parse_url($_SERVER["REQUEST_URI"]); if (file_exists('.' . $url['path'])) { // Serve the requested resource as-is. return FALSE; } // We set the base_url so that Drupal generates correct URLs for runserver // (e.g. http://127.0.0.1:8888/...), but can still select and serve a specific // site in a multisite configuration (e.g. http://mysite.com/...). $base_url = runserver_env('RUNSERVER_BASE_URL'); // The built in webserver incorrectly sets $_SERVER['SCRIPT_NAME'] when URLs // contain multiple dots (such as config entity IDs) in the path. Since this is // a virtual resource, served by index.php set the script name explicitly. // See https://github.com/drush-ops/drush/issues/2033 for more information. $_SERVER['SCRIPT_NAME'] = '/index.php'; // Include the main index.php and let Drupal take over. // n.b. Drush sets the cwd to the Drupal root during bootstrap. include 'index.php'; uid; } else { $uid = $log_entry['user']->id(); } $message = strtr('Watchdog: !message | severity: !severity | type: !type | uid: !uid | !ip | !request_uri | !referer | !link', array( '!message' => strip_tags(!isset($log_entry['variables']) ? $log_entry['message'] : strtr($log_entry['message'], $log_entry['variables'])), '!severity' => $log_entry['severity'], '!type' => $log_entry['type'], '!ip' => $log_entry['ip'], '!request_uri' => $log_entry['request_uri'], '!referer' => $log_entry['referer'], '!uid' => $uid, '!link' => strip_tags($log_entry['link']), )); error_log($message); } } // Get a $_SERVER key, or equivalent environment variable // if it is not set in $_SERVER. function runserver_env($key) { if (isset($_SERVER[$key])) { return $_SERVER[$key]; } else { return getenv($key); } } 'Runs PHP\'s built-in http server for development.', 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_FULL, 'arguments' => array( 'addr:port/path' => 'Host IP address and port number to bind to and path to open in web browser. Format is addr:port/path, default 127.0.0.1:8888, all elements optional. See examples for shorthand. Only opens a browser if a path is specified.', ), 'options' => array( 'variables' => 'Key-value array of variables to override in the $conf array for the running site. By default disables drupal_http_request_fails to avoid errors on Windows (which supports only one connection at a time). Comma delimited list of name=value pairs (or array in drushrc).', 'default-server' => 'A default addr:port/path to use for any values not specified as an argument.', 'user' => 'If opening a web browser, automatically log in as this user (user ID or username). Default is to log in as uid 1.', 'browser' => 'If opening a web browser, which browser to user (defaults to operating system default). Use --no-browser to avoid opening a browser.', 'dns' => 'Resolve hostnames/IPs using DNS/rDNS (if possible) to determine binding IPs and/or human friendly hostnames for URLs and browser.', ), 'aliases' => array('rs'), 'examples' => array( 'drush rs 8080' => 'Start runserver on 127.0.0.1, port 8080.', 'drush rs 10.0.0.28:80' => 'Start runserver on 10.0.0.28, port 80.', 'drush rs [::1]:80' => 'Start runserver on IPv6 localhost ::1, port 80.', 'drush rs --dns localhost:8888/user' => 'Start runserver on localhost (using rDNS to determine binding IP), port 8888, and open /user in browser.', 'drush rs /' => 'Start runserver on default IP/port (127.0.0.1, port 8888), and open / in browser.', 'drush rs --default-server=127.0.0.1:8080/ -' => 'Use a default (would be specified in your drushrc) that starts runserver on port 8080, and opens a browser to the front page. Set path to a single hyphen path in argument to prevent opening browser for this session.', 'drush rs :9000/admin' => 'Start runserver on 127.0.0.1, port 9000, and open /admin in browser. Note that you need a colon when you specify port and path, but no IP.', ), ); return $items; } /** * Callback for runserver command. */ function drush_core_runserver($uri = NULL) { global $user, $base_url; // Determine active configuration. $uri = runserver_uri($uri); if (!$uri) { return FALSE; } // Remove any leading slashes from the path, since that is what url() expects. $path = ltrim($uri['path'], '/'); // $uri['addr'] is a special field set by runserver_uri() $hostname = $uri['host']; $addr = $uri['addr']; drush_set_context('DRUSH_URI', 'http://' . $hostname . ':' . $uri['port']); // We pass in the currently logged in user (if set via the --user option), // which will automatically log this user in the browser during the first // request. if (drush_get_option('user', FALSE) === FALSE) { drush_set_option('user', 1); } drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_LOGIN); // We delete any registered files here, since they are not caught by Ctrl-C. _drush_delete_registered_files(); // We set the effective base_url, since we have now detected the current site, // and need to ensure generated URLs point to our runserver host. // We also pass in the effective base_url to our auto_prepend_script via the // CGI environment. This allows Drupal to generate working URLs to this http // server, whilst finding the correct multisite from the HTTP_HOST header. $base_url = 'http://' . $addr . ':' . $uri['port']; $env['RUNSERVER_BASE_URL'] = $base_url; // We pass in an array of $conf overrides using the same approach. // This is available as an option for developers to pass in their own // favorite $conf overrides (e.g. disabling css aggregation). $current_override = drush_get_option_list('variables', array()); $override = array(); foreach ($current_override as $name => $value) { if (is_numeric($name) && (strpos($value, '=') !== FALSE)) { list($name, $value) = explode('=', $value, 2); } $override[$name] = $value; } $env['RUNSERVER_CONF'] = urlencode(serialize($override)); // We log in with the specified user ID (if set) via the password reset URL. $user_message = ''; $usersingle = drush_user_get_class()->getCurrentUserAsSingle(); if ($usersingle->id()) { $browse = $usersingle->passResetUrl($path); $user_message = ', logged in as ' . $usersingle->getUsername(); } else { $browse = drush_url($path); } drush_print(dt('HTTP server listening on !addr, port !port (see http://!hostname:!port/!path), serving site !site!user...', array('!addr' => $addr, '!hostname' => $hostname, '!port' => $uri['port'], '!path' => $path, '!site' => drush_get_context('DRUSH_DRUPAL_SITE', 'default'), '!user' => $user_message))); // Start php 5.4 builtin server. // Store data used by runserver-prepend.php in the shell environment. foreach ($env as $key => $value) { putenv($key . '=' . $value); } if (!empty($uri['path'])) { // Start a browser if desired. Include a 2 second delay to allow the // server to come up. drush_start_browser($browse, 2); } // Start the server using 'php -S'. if (drush_drupal_major_version() >= 8) { $extra = ' "' . __DIR__ . '/d8-rs-router.php"'; } elseif (drush_drupal_major_version() == 7) { $extra = ' "' . __DIR__ . '/d7-rs-router.php"'; } else { $extra = ' --define auto_prepend_file="' . __DIR__ . '/runserver-prepend.php"'; } $root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); drush_shell_exec_interactive('cd %s && %s -S ' . $addr . ':' . $uri['port']. $extra, $root, drush_get_option('php', 'php')); } /** * Determine the URI to use for this server. */ function runserver_uri($uri) { $drush_default = array( 'host' => '127.0.0.1', 'port' => '8888', 'path' => '', ); $user_default = runserver_parse_uri(drush_get_option('default-server', '')); $site_default = runserver_parse_uri(drush_get_option('uri', '')); $uri = runserver_parse_uri($uri); if (is_array($uri)) { // Populate defaults. $uri = $uri + $user_default + $site_default + $drush_default; if (ltrim($uri['path'], '/') == '-') { // Allow a path of a single hyphen to clear a default path. $uri['path'] = ''; } // Determine and set the new URI. $uri['addr'] = $uri['host']; if (drush_get_option('dns', FALSE)) { if (ip2long($uri['host'])) { $uri['host'] = gethostbyaddr($uri['host']); } else { $uri['addr'] = gethostbyname($uri['host']); } } } return $uri; } /** * Parse a URI or partial URI (including just a port, host IP or path). * * @param string $uri * String that can contain partial URI. * * @return array * URI array as returned by parse_url. */ function runserver_parse_uri($uri) { if (empty($uri)) { return array(); } if ($uri[0] == ':') { // ':port/path' shorthand, insert a placeholder hostname to allow parsing. $uri = 'placeholder-hostname' . $uri; } // FILTER_VALIDATE_IP expects '[' and ']' to be removed from IPv6 addresses. // We check for colon from the right, since IPv6 addresses contain colons. $to_path = trim(substr($uri, 0, strpos($uri, '/')), '[]'); $to_port = trim(substr($uri, 0, strrpos($uri, ':')), '[]'); if (filter_var(trim($uri, '[]'), FILTER_VALIDATE_IP) || filter_var($to_path, FILTER_VALIDATE_IP) || filter_var($to_port, FILTER_VALIDATE_IP)) { // 'IP', 'IP/path' or 'IP:port' shorthand, insert a schema to allow parsing. $uri = 'http://' . $uri; } $uri = parse_url($uri); if (empty($uri)) { return drush_set_error('RUNSERVER_INVALID_ADDRPORT', dt('Invalid argument - should be in the "host:port/path" format, numeric (port only) or non-numeric (path only).')); } if (count($uri) == 1 && isset($uri['path'])) { if (is_numeric($uri['path'])) { // Port only shorthand. $uri['port'] = $uri['path']; unset($uri['path']); } } if (isset($uri['host']) && $uri['host'] == 'placeholder-hostname') { unset($uri['host']); } return $uri; } 'The DB connection key if using multiple connections in settings.php.', 'example-value' => 'key', ); $db_url['db-url'] = array( 'description' => 'A Drupal 6 style database URL.', 'example-value' => 'mysql://root:pass@127.0.0.1/db', ); $options['target'] = array( 'description' => 'The name of a target within the specified database connection. Defaults to \'default\'.', 'example-value' => 'key', // Gets unhidden in help_alter(). We only want to show this to D7 users but have to // declare it here since some commands do not bootstrap fully. 'hidden' => TRUE, ); $items['sql-drop'] = array( 'description' => 'Drop all tables in a given database.', 'arguments' => array( ), 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'options' => array( 'yes' => 'Skip confirmation and proceed.', 'result-file' => array( 'description' => 'Save to a file. The file should be relative to Drupal root. Recommended.', 'example-value' => '/path/to/file', ), ) + $options + $db_url, 'topics' => array('docs-policy'), ); $items['sql-conf'] = array( 'description' => 'Print database connection details using print_r().', 'hidden' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'options' => array( 'all' => 'Show all database connections, instead of just one.', 'show-passwords' => 'Show database password.', ) + $options, 'outputformat' => array( 'default' => 'print-r', 'pipe-format' => 'var_export', 'private-fields' => 'password', ), ); $items['sql-connect'] = array( 'description' => 'A string for connecting to the DB.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'options' => $options + $db_url + array( 'extra' => array( 'description' => 'Add custom options to the connect string.', 'example-value' => '--skip-column-names', ), ), 'examples' => array( '`drush sql-connect` < example.sql' => 'Bash: Import SQL statements from a file into the current database.', 'eval (drush sql-connect) < example.sql' => 'Fish: Import SQL statements from a file into the current database.', ), ); $items['sql-create'] = array( 'description' => 'Create a database.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'examples' => array( 'drush sql-create' => 'Create the database for the current site.', 'drush @site.test sql-create' => 'Create the database as specified for @site.test.', 'drush sql-create --db-su=root --db-su-pw=rootpassword --db-url="mysql://drupal_db_user:drupal_db_password@127.0.0.1/drupal_db"' => 'Create the database as specified in the db-url option.' ), 'options' => array( 'db-su' => 'Account to use when creating a new database. Optional.', 'db-su-pw' => 'Password for the "db-su" account. Optional.', ) + $options + $db_url, ); $items['sql-dump'] = array( 'description' => 'Exports the Drupal DB as SQL using mysqldump or equivalent.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'examples' => array( 'drush sql-dump --result-file=../18.sql' => 'Save SQL dump to the directory above Drupal root.', 'drush sql-dump --skip-tables-key=common' => 'Skip standard tables. @see example.drushrc.php', 'drush sql-dump --extra=--no-data' => 'Pass extra option to dump command.', ), 'options' => drush_sql_get_table_selection_options() + array( 'result-file' => array( 'description' => 'Save to a file. The file should be relative to Drupal root. If --result-file is provided with no value, then date based filename will be created under ~/drush-backups directory.', 'example-value' => '/path/to/file', 'value' => 'optional', ), 'create-db' => array('hidden' => TRUE, 'description' => 'Omit DROP TABLE statements. Postgres and Oracle only. Used by sql-sync, since including the DROP TABLE statements interfere with the import when the database is created.'), 'data-only' => 'Dump data without statements to create any of the schema.', 'ordered-dump' => 'Order by primary key and add line breaks for efficient diff in revision control. Slows down the dump. Mysql only.', 'gzip' => 'Compress the dump using the gzip program which must be in your $PATH.', 'extra' => 'Add custom options to the dump command.', ) + $options + $db_url, ); $items['sql-query'] = array( 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'description' => 'Execute a query against a database.', 'examples' => array( 'drush sql-query "SELECT * FROM users WHERE uid=1"' => 'Browse user record. Table prefixes, if used, must be added to table names by hand.', 'drush sql-query --db-prefix "SELECT * FROM {users} WHERE uid=1"' => 'Browse user record. Table prefixes are honored. Caution: curly-braces will be stripped from all portions of the query.', '`drush sql-connect` < example.sql' => 'Import sql statements from a file into the current database.', 'drush sql-query --file=example.sql' => 'Alternate way to import sql statements from a file.', ), 'arguments' => array( 'query' => 'An SQL query. Ignored if \'file\' is provided.', ), 'options' => array( 'result-file' => array( 'description' => 'Save to a file. The file should be relative to Drupal root. Optional.', 'example-value' => '/path/to/file', ), 'file' => 'Path to a file containing the SQL to be run. Gzip files are accepted.', 'extra' => array( 'description' => 'Add custom options to the database connection command.', 'example-value' => '--skip-column-names', ), 'db-prefix' => 'Enable replacement of braces in your query.', 'db-spec' => array( 'description' => 'A database specification', 'hidden' => TRUE, // Hide since this is only used with --backend calls. ) ) + $options + $db_url, 'aliases' => array('sqlq'), ); $items['sql-cli'] = array( 'description' => "Open a SQL command-line interface using Drupal's credentials.", 'bootstrap' => DRUSH_BOOTSTRAP_NONE, // 'options' => $options + $db_url, 'allow-additional-options' => array('sql-connect'), 'aliases' => array('sqlc'), 'examples' => array( 'drush sql-cli' => "Open a SQL command-line interface using Drupal's credentials.", 'drush sql-cli --extra=-A' => "Open a SQL CLI and skip reading table information.", ), 'remote-tty' => TRUE, ); return $items; } /** * Implements hook_drush_help_alter(). */ function sql_drush_help_alter(&$command) { // Drupal 7+ only options. if (drush_drupal_major_version() >= 7) { if ($command['commandfile'] == 'sql') { unset($command['options']['target']['hidden']); } } } /** * Safely bootstrap Drupal to the point where we can * access the database configuration. */ function drush_sql_bootstrap_database_configuration() { // Under Drupal 7, if the database is configured but empty, then // DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION will throw an exception. // If this happens, we'll just catch it and continue. // TODO: Fix this in the bootstrap, per http://drupal.org/node/1996004 try { drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION); } catch (Exception $e) { } } /** * Check whether further bootstrap is needed. If so, do it. */ function drush_sql_bootstrap_further() { if (!drush_get_option(array('db-url', 'db-spec'))) { drush_sql_bootstrap_database_configuration(); } } /** * Command callback. Displays the Drupal site's database connection string. */ function drush_sql_conf() { drush_sql_bootstrap_database_configuration(); if (drush_get_option('all')) { $sqlVersion = drush_sql_get_version(); return $sqlVersion->getAll(); } else { $sql = drush_sql_get_class(); return $sql->db_spec(); } } /** * Command callback. Emits a connect string. */ function drush_sql_connect() { drush_sql_bootstrap_further(); $sql = drush_sql_get_class(); return $sql->connect(FALSE); } /** * Command callback. Create a database. */ function drush_sql_create() { drush_sql_bootstrap_further(); $sql = drush_sql_get_class(); $db_spec = $sql->db_spec(); // Prompt for confirmation. if (!drush_get_context('DRUSH_SIMULATE')) { // @todo odd - maybe for sql-sync. $txt_destination = (isset($db_spec['remote-host']) ? $db_spec['remote-host'] . '/' : '') . $db_spec['database']; drush_print(dt("Creating database !target. Any possible existing database will be dropped!", array('!target' => $txt_destination))); if (!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } } return $sql->createdb(TRUE); } /** * Command callback. Outputs the entire Drupal database in SQL format using mysqldump or equivalent. */ function drush_sql_dump() { drush_sql_bootstrap_further(); $sql = drush_sql_get_class(); return $sql->dump(drush_get_option('result-file', FALSE)); } /** * Construct an array that places table names in appropriate * buckets based on whether the table is to be skipped, included * for structure only, or have structure and data dumped. * The keys of the array are: * - skip: tables to be skipped completed in the dump * - structure: tables to only have their structure i.e. DDL dumped * - tables: tables to have structure and data dumped * * @return array * An array of table names with each table name in the appropriate * element of the array. */ function drush_sql_get_table_selection() { // Skip large core tables if instructed. Used by 'sql-drop/sql-dump/sql-sync' commands. $skip_tables = _drush_sql_get_raw_table_list('skip-tables'); // Skip any structure-tables as well. $structure_tables = _drush_sql_get_raw_table_list('structure-tables'); // Dump only the specified tables. Takes precedence over skip-tables and structure-tables. $tables = _drush_sql_get_raw_table_list('tables'); return array('skip' => $skip_tables, 'structure' => $structure_tables, 'tables' => $tables); } function drush_sql_get_table_selection_options() { return array( 'skip-tables-key' => 'A key in the $skip_tables array. @see example.drushrc.php. Optional.', 'structure-tables-key' => 'A key in the $structure_tables array. @see example.drushrc.php. Optional.', 'tables-key' => 'A key in the $tables array. Optional.', 'skip-tables-list' => 'A comma-separated list of tables to exclude completely. Optional.', 'structure-tables-list' => 'A comma-separated list of tables to include for structure, but not data. Optional.', 'tables-list' => 'A comma-separated list of tables to transfer. Optional.', ); } /** * Expand wildcard tables. * * @param array $tables * An array of table names, some of which may contain wildcards (`*`). * @param array $db_tables * An array with all the existing table names in the current database. * @return * $tables array with wildcards resolved to real table names. */ function drush_sql_expand_wildcard_tables($tables, $db_tables) { // Table name expansion based on `*` wildcard. $expanded_db_tables = array(); foreach ($tables as $k => $table) { // Only deal with table names containing a wildcard. if (strpos($table, '*') !== FALSE) { $pattern = '/^' . str_replace('*', '.*', $table) . '$/i'; // Merge those existing tables which match the pattern with the rest of // the expanded table names. $expanded_db_tables += preg_grep($pattern, $db_tables); } } return $expanded_db_tables; } /** * Filters tables. * * @param array $tables * An array of table names to filter. * @param array $db_tables * An array with all the existing table names in the current database. * @return * An array with only valid table names (i.e. all of which actually exist in * the database). */ function drush_sql_filter_tables($tables, $db_tables) { // Ensure all the tables actually exist in the database. foreach ($tables as $k => $table) { if (!in_array($table, $db_tables)) { unset($tables[$k]); } } return $tables; } /** * Given the table names in the input array that may contain wildcards (`*`), * expand the table names so that the array returned only contains table names * that exist in the database. * * @param array $tables * An array of table names where the table names may contain the * `*` wildcard character. * @param array $db_tables * The list of tables present in a database. * @return array * An array of tables with non-existant tables removed. */ function _drush_sql_expand_and_filter_tables($tables, $db_tables) { $expanded_tables = drush_sql_expand_wildcard_tables($tables, $db_tables); $tables = drush_sql_filter_tables(array_merge($tables, $expanded_tables), $db_tables); $tables = array_unique($tables); sort($tables); return $tables; } /** * Consult the specified options and return the list of tables * specified. * * @param option_name * The option name to check: skip-tables, structure-tables * or tables. This function will check both *-key and *-list, * and, in the case of sql-sync, will also check target-* * and source-*, to see if an alias set one of these options. * @returns array * Returns an array of tables based on the first option * found, or an empty array if there were no matches. */ function _drush_sql_get_raw_table_list($option_name) { foreach(array('' => 'cli', 'target-,,source-' => NULL) as $prefix_list => $context) { foreach(explode(',',$prefix_list) as $prefix) { $key_list = drush_get_option($prefix . $option_name . '-key', NULL, $context); foreach(explode(',', $key_list) as $key) { $all_tables = drush_get_option($option_name, array()); if (array_key_exists($key, $all_tables)) { return $all_tables[$key]; } if ($option_name != 'tables') { $all_tables = drush_get_option('tables', array()); if (array_key_exists($key, $all_tables)) { return $all_tables[$key]; } } } $table_list = drush_get_option($prefix . $option_name . '-list', NULL, $context); if (isset($table_list)) { return empty($table_list) ? array() : explode(',', $table_list); } } } return array(); } /** * Command callback. Executes the given SQL query on the Drupal database. */ function drush_sql_query($query = NULL) { drush_sql_bootstrap_further(); $filename = drush_get_option('file', NULL); // Enable prefix processing when db-prefix option is used. if (drush_get_option('db-prefix')) { drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_DATABASE); } if (drush_get_context('DRUSH_SIMULATE')) { if ($query) { drush_print(dt('Simulating sql-query: !q', array('!q' => $query))); } else { drush_print(dt('Simulating sql-import from !f', array('!f' => drush_get_option('file')))); } } else { $sql = drush_sql_get_class(drush_get_option('db-spec')); $result = $sql->query($query, $filename, drush_get_option('result-file')); if (!$result) { return drush_set_error('DRUSH_SQL_NO_QUERY', dt('Query failed.')); } drush_print(implode("\n", drush_shell_exec_output())); } return TRUE; } /** * Command callback. Drops all tables in the database. */ function drush_sql_drop() { drush_sql_bootstrap_further(); $sql = drush_sql_get_class(); $db_spec = $sql->db_spec(); if (!drush_confirm(dt('Do you really want to drop all tables in the database !db?', array('!db' => $db_spec['database'])))) { return drush_user_abort(); } $tables = $sql->listTables(); return $sql->drop($tables); } /** * Command callback. Launches console a DB backend. */ function drush_sql_cli() { drush_sql_bootstrap_further(); $sql = drush_sql_get_class(); return !(bool)drush_shell_proc_open($sql->connect()); } /** * Call from a pre-sql-sync hook to register an sql * query to be executed in the post-sql-sync hook. * @see drush_sql_pre_sql_sync() and @see drush_sql_post_sql_sync(). * * @param $id * String containing an identifier representing this * operation. This id is not actually used at the * moment, it is just used to fufill the contract * of drush contexts. * @param $message * String with the confirmation message that describes * to the user what the post-sync operation is going * to do. This confirmation message is printed out * just before the user is asked whether or not the * sql-sync operation should be continued. * @param $query * String containing the sql query to execute. If no * query is provided, then the confirmation message will * be displayed to the user, but no action will be taken * in the post-sync hook. This is useful for drush modules * that wish to provide their own post-sync hooks to fix * up the target database in other ways (e.g. through * Drupal APIs). */ function drush_sql_register_post_sync_op($id, $message, $query = NULL) { $options = drush_get_context('post-sync-ops'); $options[$id] = array('message' => $message, 'query' => $query); drush_set_context('post-sync-ops', $options); } /** * Builds a confirmation message for all post-sync operations. * * @return string * All post-sync operation messages concatenated together. */ function _drush_sql_get_post_sync_messages() { $messages = ''; $operations = drush_get_context('post-sync-ops'); if (!empty($operations)) { $messages = dt('The following operations will be done on the target database:') . "\n"; $bullets = array_column($operations, 'message'); $messages .= " * " . implode("\n * ", $bullets) . "\n"; } return $messages; } /** * Wrapper for drush_get_class; instantiates an driver-specific instance * of SqlBase class. * * @param array $db_spec * If known, specify a $db_spec that the class can operate with. * * @throws \Drush\Sql\SqlException * * @return Drush\Sql\SqlBase */ function drush_sql_get_class($db_spec = NULL) { $database = drush_get_option('database', 'default'); $target = drush_get_option('target', 'default'); // Try a few times to quickly get $db_spec. if (!empty($db_spec)) { if (!empty($db_spec['driver'])) { return drush_get_class(array('Drush\Sql\Sql', 'Drupal\Driver\Database\\' . $db_spec['driver'] . '\Drush'), array($db_spec), array($db_spec['driver'])); } } elseif ($url = drush_get_option('db-url')) { $url = is_array($url) ? $url[$database] : $url; $db_spec = drush_convert_db_from_db_url($url); $db_spec['db_prefix'] = drush_get_option('db-prefix'); return drush_sql_get_class($db_spec); } elseif (($databases = drush_get_option('databases')) && (array_key_exists($database, $databases)) && (array_key_exists($target, $databases[$database]))) { $db_spec = $databases[$database][$target]; return drush_sql_get_class($db_spec); } else { // No parameter or options provided. Determine $db_spec ourselves. if ($sqlVersion = drush_sql_get_version()) { if ($db_spec = $sqlVersion->get_db_spec()) { return drush_sql_get_class($db_spec); } } } throw new \Drush\Sql\SqlException('Unable to find a matching SQL Class. Drush cannot find your database connection details.'); } /** * Wrapper for drush_get_class; instantiates a Drupal version-specific instance * of SqlVersion class. * * @return Drush\Sql\SqlVersion */ function drush_sql_get_version() { return drush_get_class('Drush\Sql\Sql', array(), array(drush_drupal_major_version())) ?: NULL; } /** * Implements hook_drush_sql_sync_sanitize(). * * Sanitize usernames, passwords, and sessions when the --sanitize option is used. * It is also an example of how to write a database sanitizer for sql sync. * * To write your own sync hook function, define mymodule_drush_sql_sync_sanitize() * in mymodule.drush.inc and follow the form of this function to add your own * database sanitization operations via the register post-sync op function; * @see drush_sql_register_post_sync_op(). This is the only thing that the * sync hook function needs to do; sql-sync takes care of the rest. * * The function below has a lot of logic to process user preferences and * generate the correct SQL regardless of whether Postgres, Mysql, * Drupal 6/7/8 is in use. A simpler sanitize function that * always used default values and only worked with Drupal 6 + mysql * appears in the drush.api.php. @see hook_drush_sql_sync_sanitize(). */ function sql_drush_sql_sync_sanitize($site) { $site_settings = drush_sitealias_get_record($site); $databases = sitealias_get_databases_from_record($site_settings); $major_version = drush_drupal_major_version(); $wrap_table_name = (bool) drush_get_option('db-prefix'); $user_table_updates = array(); $message_list = array(); // Sanitize passwords. $newpassword = drush_get_option(array('sanitize-password', 'destination-sanitize-password'), 'password'); if ($newpassword != 'no' && $newpassword !== 0) { $pw_op = ""; // In Drupal 6, passwords are hashed via the MD5 algorithm. if ($major_version == 6) { $pw_op = "MD5('$newpassword')"; } // In Drupal 7, passwords are hashed via a more complex algorithm, // available via the user_hash_password function. elseif ($major_version == 7) { $core = DRUSH_DRUPAL_CORE; include_once $core . '/includes/password.inc'; include_once $core . '/includes/bootstrap.inc'; $hash = user_hash_password($newpassword); $pw_op = "'$hash'"; } else { // D8+. Mimic Drupal's /scripts/password-hash.sh drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_FULL); $password_hasher = \Drupal::service('password'); $hash = $password_hasher->hash($newpassword); $pw_op = "'$hash'"; } if (!empty($pw_op)) { $user_table_updates[] = "pass = $pw_op"; $message_list[] = "passwords"; } } // Sanitize email addresses. $newemail = drush_get_option(array('sanitize-email', 'destination-sanitize-email'), 'user+%uid@localhost.localdomain'); if ($newemail != 'no' && $newemail !== 0) { if (strpos($newemail, '%') !== FALSE) { // We need a different sanitization query for Postgres and Mysql. $db_driver = $databases['default']['default']['driver']; if ($db_driver == 'pgsql') { $email_map = array('%uid' => "' || uid || '", '%mail' => "' || replace(mail, '@', '_') || '", '%name' => "' || replace(name, ' ', '_') || '"); $newmail = "'" . str_replace(array_keys($email_map), array_values($email_map), $newemail) . "'"; } elseif ($db_driver == 'mssql') { $email_map = array('%uid' => "' + uid + '", '%mail' => "' + replace(mail, '@', '_') + '", '%name' => "' + replace(name, ' ', '_') + '"); $newmail = "'" . str_replace(array_keys($email_map), array_values($email_map), $newemail) . "'"; } else { $email_map = array('%uid' => "', uid, '", '%mail' => "', replace(mail, '@', '_'), '", '%name' => "', replace(name, ' ', '_'), '"); $newmail = "concat('" . str_replace(array_keys($email_map), array_values($email_map), $newemail) . "')"; } $user_table_updates[] = "mail = $newmail, init = $newmail"; } else { $user_table_updates[] = "mail = '$newemail', init = '$newemail'"; } $message_list[] = 'email addresses'; } if (!empty($user_table_updates)) { $table = $major_version >= 8 ? 'users_field_data' : 'users'; if ($wrap_table_name) { $table = "{{$table}}"; } $sanitize_query = "UPDATE {$table} SET " . implode(', ', $user_table_updates) . " WHERE uid > 0;"; drush_sql_register_post_sync_op('user-email', dt('Reset !message in !table table', array('!message' => implode(' and ', $message_list), '!table' => $table)), $sanitize_query); } $sanitizer = new \Drush\Commands\core\SanitizeCommands(); $sanitizer->doSanitize($major_version); } 'Copies the database contents from a source site to a target site. Transfers the database dump via rsync.', 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'drush dependencies' => array('sql', 'core'), // core-rsync. 'package' => 'sql', 'examples' => array( 'drush sql-sync @source @target' => 'Copy the database from the site with the alias "source" to the site with the alias "target".', 'drush sql-sync prod dev' => 'Copy the database from the site in /sites/prod to the site in /sites/dev (multisite installation).', ), 'arguments' => array( 'source' => 'A site-alias or the name of a subdirectory within /sites whose database you want to copy from.', 'target' => 'A site-alias or the name of a subdirectory within /sites whose database you want to replace.', ), 'required-arguments' => TRUE, 'options' => drush_sql_get_table_selection_options() + array( // 'cache' => 'Skip dump if result file exists and is less than "cache" hours old. Optional; default is 24 hours.', // 'no-cache' => 'Do not cache the sql-dump file.', 'no-dump' => 'Do not dump the sql database; always use an existing dump file.', 'no-sync' => 'Do not rsync the database dump file from source to target.', 'runner' => 'Where to run the rsync command; defaults to the local site. Can also be "source" or "destination".', 'source-db-url' => 'Database specification for source system to dump from.', 'source-remote-port' => 'Override sql database port number in source-db-url. Optional.', 'source-remote-host' => 'Remote machine to run sql-dump file on. Optional; default is local machine.', 'source-dump' => array( 'description' => 'The destination for the dump file, or the path to the dump file when --no-dump is specified.', 'example-value' => '/dumpdir/db.sql', ), 'source-database' => 'A key in the $db_url (D6) or $databases (D7+) array which provides the data.', 'source-target' => array( 'description' => 'A key within the SOURCE database identifying a particular server in the database group.', 'example-value' => 'key', // Gets unhidden in help_alter(). We only want to show to D7+ users but have to // declare it here since this command does not bootstrap fully. 'hidden' => TRUE, ), 'target-db-url' => '', 'target-remote-port' => '', 'target-remote-host' => '', 'target-dump' => array( 'description' => 'A path for saving the dump file on target. Mandatory when using --no-sync.', 'example-value' => '/dumpdir/db.sql.gz', ), 'target-database' => 'A key in the $db_url (D6) or $databases (D7+) array which shall receive the data.', 'target-target' => array( 'description' => 'Oy. A key within the TARGET database identifying a particular server in the database group.', 'example-value' => 'key', // Gets unhidden in help_alter(). We only want to show to D7+ users but have to // declare it here since this command does not bootstrap fully. 'hidden' => TRUE, ), 'create-db' => 'Create a new database before importing the database dump on the target machine.', 'db-su' => array( 'description' => 'Account to use when creating a new database. Optional.', 'example-value' => 'root', ), 'db-su-pw' => array( 'description' => 'Password for the "db-su" account. Optional.', 'example-value' => 'pass', ), // 'no-ordered-dump' => 'Do not pass --ordered-dump to sql-dump. sql-sync orders the dumpfile by default in order to increase the efficiency of rsync.', 'sanitize' => 'Obscure email addresses and reset passwords in the user table post-sync.', ), 'sub-options' => array( 'sanitize' => drupal_sanitize_options() + array( 'confirm-sanitizations' => 'Prompt yes/no after importing the database, but before running the sanitizations', ), ), 'topics' => array('docs-aliases', 'docs-policy', 'docs-example-sync-via-http', 'docs-example-sync-extension'), ); return $items; } /** * Implements hook_drush_help_alter(). */ function sqlsync_drush_help_alter(&$command) { // Drupal 7+ only options. if (drush_drupal_major_version() >= 7) { if ($command['command'] == 'sql-sync') { unset($command['options']['source-target']['hidden'], $command['options']['target-target']['hidden']); } } } /** * Command argument complete callback. * * @return * Array of available site aliases. */ function sql_sql_sync_complete() { return array('values' => array_keys(_drush_sitealias_all_list())); } /* * Implements COMMAND hook init. */ function drush_sql_sync_init($source, $destination) { // Try to get @self defined when --uri was not provided. drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_SITE); // Preflight destination in case it defines the alias used by the source _drush_sitealias_get_record($destination); // After preflight, get source and destination settings $source_settings = drush_sitealias_get_record($source); $destination_settings = drush_sitealias_get_record($destination); // Apply command-specific options. drush_sitealias_command_default_options($source_settings, 'source-'); drush_sitealias_command_default_options($destination_settings, 'target-'); } /* * A command validate callback. */ function drush_sqlsync_sql_sync_validate($source, $destination) { // Get destination info for confirmation prompt. $source_settings = drush_sitealias_overlay_options(drush_sitealias_get_record($source), 'source-'); $destination_settings = drush_sitealias_overlay_options(drush_sitealias_get_record($destination), 'target-'); $source_db_spec = drush_sitealias_get_db_spec($source_settings, FALSE, 'source-'); $target_db_spec = drush_sitealias_get_db_spec($destination_settings, FALSE, 'target-'); $txt_source = (isset($source_db_spec['remote-host']) ? $source_db_spec['remote-host'] . '/' : '') . $source_db_spec['database']; $txt_destination = (isset($target_db_spec['remote-host']) ? $target_db_spec['remote-host'] . '/' : '') . $target_db_spec['database']; // Validate. if (empty($source_db_spec)) { if (empty($source_settings)) { return drush_set_error('DRUSH_ALIAS_NOT_FOUND', dt('Error: no alias record could be found for source !source', array('!source' => $source))); } return drush_set_error('DRUSH_DATABASE_NOT_FOUND', dt('Error: no database record could be found for source !source', array('!source' => $source))); } if (empty($target_db_spec)) { if (empty($destination_settings)) { return drush_set_error('DRUSH_ALIAS_NOT_FOUND', dt('Error: no alias record could be found for target !destination', array('!destination' => $destination))); } return drush_set_error('DRUSH_DATABASE_NOT_FOUND', dt('Error: no database record could be found for target !destination', array('!destination' => $destination))); } if (drush_get_option('no-dump') && !drush_get_option('source-dump')) { return drush_set_error('DRUSH_SOURCE_DUMP_MISSING', dt('The --source-dump option must be supplied when --no-dump is specified.')); } if (drush_get_option('no-sync') && !drush_get_option('target-dump')) { return drush_set_error('DRUSH_TARGET_DUMP_MISSING', dt('The --target-dump option must be supplied when --no-sync is specified.')); } if (!drush_get_context('DRUSH_SIMULATE')) { drush_print(dt("You will destroy data in !target and replace with data from !source.", array('!source' => $txt_source, '!target' => $txt_destination))); // @todo Move sanitization prompts to here. They currently show much later. if (!drush_confirm(dt('Do you really want to continue?'))) { return drush_user_abort(); } } } /* * A command callback. */ function drush_sqlsync_sql_sync($source, $destination) { $source_settings = drush_sitealias_overlay_options(drush_sitealias_get_record($source), 'source-'); $destination_settings = drush_sitealias_overlay_options(drush_sitealias_get_record($destination), 'target-'); $source_is_local = !array_key_exists('remote-host', $source_settings) || drush_is_local_host($source_settings); $destination_is_local = !array_key_exists('remote-host', $destination_settings) || drush_is_local_host($destination_settings); // These options are passed along to subcommands like sql-create, sql-dump, sql-query, sql-sanitize, ... $source_options = drush_get_merged_prefixed_options('source-'); $target_options = drush_get_merged_prefixed_options('target-'); $backend_options = array(); // @todo drush_redispatch_get_options() assumes you will execute same command. Not good. $global_options = drush_redispatch_get_options() + array( 'strict' => 0, ); // We do not want to include root or uri here. If the user // provided -r or -l, their key has already been remapped to // 'root' or 'uri' by the time we get here. unset($global_options['root']); unset($global_options['uri']); if (drush_get_context('DRUSH_SIMULATE')) { $backend_options['backend-simulate'] = TRUE; } // Create destination DB if needed. if (drush_get_option('create-db')) { drush_log(dt('Starting to create database on Destination.'), LogLevel::OK); $return = drush_invoke_process($destination, 'sql-create', array(), $global_options + $target_options, $backend_options); if ($return['error_status']) { return drush_set_error('DRUSH_SQL_CREATE_FAILED', dt('sql-create failed.')); } } // Perform sql-dump on source unless told otherwise. $options = $global_options + $source_options + array( 'gzip' => TRUE, 'result-file' => drush_get_option('source-dump', TRUE), // 'structure-tables-list' => 'cache*', // Do we want to default to this? ); if (!drush_get_option('no-dump')) { drush_log(dt('Starting to dump database on Source.'), LogLevel::OK); $return = drush_invoke_process($source, 'sql-dump', array(), $options, $backend_options); if ($return['error_status']) { return drush_set_error('DRUSH_SQL_DUMP_FAILED', dt('sql-dump failed.')); } else { $source_dump_path = $return['object']; if (!is_string($source_dump_path)) { return drush_set_error('DRUSH_SQL_DUMP_FILE_NOT_REPORTED', dt('The Drush sql-dump command did not report the path to the dump file produced. Try upgrading the version of Drush you are using on the source machine.')); } } } else { $source_dump_path = drush_get_option('source-dump'); } $do_rsync = !drush_get_option('no-sync'); // Determine path/to/dump on destination. if (drush_get_option('target-dump')) { $destination_dump_path = drush_get_option('target-dump'); $rsync_options['yes'] = TRUE; // @temporary: See https://github.com/drush-ops/drush/pull/555 } elseif ($source_is_local && $destination_is_local) { $destination_dump_path = $source_dump_path; $do_rsync = FALSE; } else { $tmp = '/tmp'; // Our fallback plan. drush_log(dt('Starting to discover temporary files directory on Destination.'), LogLevel::OK); $return = drush_invoke_process($destination, 'core-status', array(), array(), array('integrate' => FALSE, 'override-simulated' => TRUE)); if (!$return['error_status'] && isset($return['object']['drush-temp'])) { $tmp = $return['object']['drush-temp']; } $destination_dump_path = Path::join($tmp, basename($source_dump_path)); $rsync_options['yes'] = TRUE; // No need to prompt as destination is a tmp file. } if ($do_rsync) { if (!drush_get_option('no-dump')) { // Cleanup if this command created the dump file. $rsync_options['remove-source-files'] = TRUE; } $runner = drush_get_runner($source_settings, $destination_settings, drush_get_option('runner', FALSE)); // Since core-rsync is a strict-handling command and drush_invoke_process() puts options at end, we can't send along cli options to rsync. // Alternatively, add options like --ssh-options to a site alias (usually on the machine that initiates the sql-sync). $return = drush_invoke_process($runner, 'core-rsync', array("$source:$source_dump_path", "$destination:$destination_dump_path"), $rsync_options); drush_log(dt('Copying dump file from Source to Destination.'), LogLevel::OK); if ($return['error_status']) { return drush_set_error('DRUSH_RSYNC_FAILED', dt('core-rsync failed.')); } } // Import file into destination. drush_log(dt('Starting to import dump file onto Destination database.'), LogLevel::OK); $options = $global_options + $target_options + array( 'file' => $destination_dump_path, 'file-delete' => TRUE, ); $return = drush_invoke_process($destination, 'sql-query', array(), $options, $backend_options); if ($return['error_status']) { // An error was already logged. return FALSE; } // Run Sanitize if needed. $options = $global_options + $target_options; if (drush_get_option('sanitize')) { drush_log(dt('Starting to sanitize target database on Destination.'), LogLevel::OK); $return = drush_invoke_process($destination, 'sql-sanitize', array(), $options, $backend_options); if ($return['error_status']) { return drush_set_error('DRUSH_SQL_SANITIZE_FAILED', dt('sql-sanitize failed.')); } } } array( 'description' => 'A comma delimited list of uids of users to operate on.', 'example-value' => '3,5', 'value' => 'required', ), 'name' => array( 'description' => 'A comma delimited list of user names of users to operate on.', 'example-value' => 'foo', 'value' => 'required', ), 'mail' => array( 'description' => 'A comma delimited list of user mail addresses of users to operate on.', 'example-value' => 'me@example.com', 'value' => 'required', ) ); $items['user-information'] = array( 'description' => 'Print information about the specified user(s).', 'aliases' => array('uinf'), 'examples' => array( 'drush user-information 2,3,someguy,somegal,billgates@microsoft.com' => 'Display information about the listed users.', ), 'arguments' => array( 'users' => 'A comma delimited list of uids, user names, or email addresses.', ), 'required-arguments' => TRUE, 'outputformat' => array( 'default' => 'key-value-list', 'pipe-format' => 'csv', 'field-labels' => array( 'uid' => 'User ID', 'name' => 'User name', 'pass' => 'Password', 'mail' => 'User mail', 'theme' => 'User theme', 'signature' => 'Signature', 'signature_format' => 'Signature format', 'user_created' => 'User created', 'created' => 'Created', 'user_access' => 'User last access', 'access' => 'Last access', 'user_login' => 'User last login', 'login' => 'Last login', 'user_status' => 'User status', 'status' => 'Status', 'timezone' => 'Time zone', 'picture' => 'User picture', 'init' => 'Initial user mail', 'roles' => 'User roles', 'group_audience' => 'Group Audience', 'langcode' => 'Language code', 'uuid' => 'Uuid', ), 'format-cell' => 'csv', 'fields-default' => array('uid', 'name', 'mail', 'roles', 'user_status'), 'fields-pipe' => array('name', 'uid', 'mail', 'status', 'roles'), 'fields-full' => array('uid', 'name', 'pass', 'mail', 'theme', 'signature', 'user_created', 'user_access', 'user_login', 'user_status', 'timezone', 'roles', 'group_audience', 'langcode', 'uuid'), 'output-data-type' => 'format-table', ), ); $items['user-block'] = array( 'description' => 'Block the specified user(s).', 'aliases' => array('ublk'), 'arguments' => array( 'users' => 'A comma delimited list of uids, user names, or email addresses.', ), 'examples' => array( 'drush user-block 5,user3 --uid=2,3 --name=someguy,somegal --mail=billgates@microsoft.com' => 'Block the users with name, id, or email 5 or user3, uids 2 and 3, names someguy and somegal, and email address of billgates@microsoft.com', ), 'options' => $options_common, ); $items['user-unblock'] = array( 'description' => 'Unblock the specified user(s).', 'aliases' => array('uublk'), 'arguments' => array( 'users' => 'A comma delimited list of uids, user names, or email addresses.', ), 'examples' => array( 'drush user-unblock 5,user3 --uid=2,3 --name=someguy,somegal --mail=billgates@microsoft.com' => 'Unblock the users with name, id, or email 5 or user3, uids 2 and 3, names someguy and somegal, and email address of billgates@microsoft.com', ), 'options' => $options_common, ); $items['user-add-role'] = array( 'description' => 'Add a role to the specified user accounts.', 'aliases' => array('urol'), 'arguments' => array( 'role' => 'The name of the role to add', 'users' => '(optional) A comma delimited list of uids, user names, or email addresses.', ), 'required-arguments' => 1, 'examples' => array( 'drush user-add-role "power user" 5,user3 --uid=2,3 --name=someguy,somegal --mail=billgates@microsoft.com' => 'Add the "power user" role to the accounts with name, id, or email 5 or user3, uids 2 and 3, names someguy and somegal, and email address of billgates@microsoft.com', ), 'options' => $options_common, ); $items['user-remove-role'] = array( 'description' => 'Remove a role from the specified user accounts.', 'aliases' => array('urrol'), 'arguments' => array( 'role' => 'The name of the role to remove', 'users' => '(optional) A comma delimited list of uids, user names, or email addresses.', ), 'required-arguments' => 1, 'examples' => array( 'drush user-remove-role "power user" 5,user3 --uid=2,3 --name=someguy,somegal --mail=billgates@microsoft.com' => 'Remove the "power user" role from the accounts with name, id, or email 5 or user3, uids 2 and 3, names someguy and somegal, and email address of billgates@microsoft.com', ), 'options' => $options_common, ); $items['user-create'] = array( 'description' => 'Create a user account with the specified name.', 'aliases' => array('ucrt'), 'arguments' => array( 'name' => 'The name of the account to add' ), 'required-arguments' => TRUE, 'examples' => array( 'drush user-create newuser --mail="person@example.com" --password="letmein"' => 'Create a new user account with the name newuser, the email address person@example.com, and the password letmein', ), 'options' => array( 'password' => 'The password for the new account', 'mail' => 'The email address for the new account', ), 'outputformat' => $items['user-information']['outputformat'], ); $items['user-cancel'] = array( 'description' => 'Cancel a user account with the specified name.', 'aliases' => array('ucan'), 'arguments' => array( 'name' => 'The name of the account to cancel', ), // The `_user_cancel` method still references global $user. // @todo remove once https://www.drupal.org/node/2163205 is in. 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_LOGIN, 'required-arguments' => TRUE, 'examples' => array( 'drush user-cancel username' => 'Cancel the user account with the name username and anonymize all content created by that user.', ), ); $items['user-password'] = array( 'description' => '(Re)Set the password for the user account with the specified name.', 'aliases' => array('upwd'), 'arguments' => array( 'name' => 'The name of the account to modify.' ), 'required-arguments' => TRUE, 'options' => array( 'password' => array( 'description' => 'The new password for the account.', 'required' => TRUE, 'example-value' => 'foo', ), ), 'examples' => array( 'drush user-password someuser --password="correct horse battery staple"' => 'Set the password for the username someuser. @see xkcd.com/936', ), ); $items['user-login'] = array( 'description' => 'Display a one time login link for the given user account (defaults to uid 1).', 'aliases' => array('uli'), 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'handle-remote-commands' => TRUE, 'arguments' => array( 'user' => 'An optional uid, user name, or email address for the user to log in as. Default is to log in as uid 1. The uid/name/mail options take priority if specified.', 'path' => 'Optional path to redirect to after logging in.', ), 'options' => array( 'browser' => 'Optional value denotes which browser to use (defaults to operating system default). Use --no-browser to suppress opening a browser.', 'uid' => 'A uid to log in as.', 'redirect-port' => 'A custom port for redirecting to (e.g. when running within a Vagrant environment)', 'name' => 'A user name to log in as.', 'mail' => 'A user mail address to log in as.', ), 'examples' => array( 'drush user-login ryan node/add/blog' => 'Displays and opens default web browser (if configured or detected) for a one-time login link for the user with the username ryan and redirect to the path node/add/blog.', 'drush user-login --browser=firefox --mail=drush@example.org admin/settings/performance' => 'Open firefox web browser, login as the user with the e-mail address drush@example.org and redirect to the path admin/settings/performance.', ), ); return $items; } /** * Implements hook_drush_help_alter(). */ function user_drush_help_alter(&$command) { // Drupal 7+ only options. if ($command['command'] == 'user-cancel' && drush_drupal_major_version() >= 7) { $command['options']['delete-content'] = 'Delete all content created by the user'; $command['examples']['drush user-cancel --delete-content username'] = 'Cancel the user account with the name username and delete all content created by that user.'; } } /** * Get a version specific UserSingle class. * * @param $account * @return \Drush\User\UserSingleBase * * @see drush_get_class(). */ function drush_usersingle_get_class($account) { return drush_get_class('Drush\User\UserSingle', array($account)); } /** * Get a version specific User class. * * @return \Drush\User\UserVersion * * @see drush_get_class(). */ function drush_user_get_class() { return drush_get_class('Drush\User\User'); } /** * Command callback. Prints information about the specified user(s). */ function drush_user_information($users) { $userlist = new UserList($users); $info = $userlist->each('info'); return $info; } /** * Block the specified user(s). */ function drush_user_block($users = '') { $userlist = new UserList($users); $userlist->each('block'); drush_log(dt('Blocked user(s): !users', array('!users' => $userlist->names())), LogLevel::SUCCESS); } /** * Unblock the specified user(s). */ function drush_user_unblock($users = '') { $userlist = new UserList($users); $userlist->each('unblock'); drush_log(dt('Unblocked user(s): !users', array('!users' => $userlist->names())), LogLevel::SUCCESS); } /** * Add a role to the specified user accounts. */ function drush_user_add_role($role, $users = '') { // If role is not found, an exception gets thrown and handled by command invoke. $role_object = drush_role_get_class($role); $userlist = new UserList($users); $userlist->each('addRole', array($role_object->rid)); drush_log(dt('Added role !role role to !users', array('!role' => $role, '!users' => $userlist->names())),LogLevel::SUCCESS); } /** * Remove a role from the specified user accounts. */ function drush_user_remove_role($role, $users = '') { // If role is not found, an exception gets thrown and handled by command invoke. $role_object = drush_role_get_class($role); $userlist = new UserList($users); $userlist->each('removeRole', array($role_object->rid)); drush_log(dt('Removed !role role from !users', array('!role' => $role, '!users' => $userlist->names())),LogLevel::SUCCESS); } /** * Creates a new user account. */ function drush_user_create($name) { $userversion = drush_user_get_class(); $mail = drush_get_option('mail'); $pass = drush_get_option('password'); $new_user = array( 'name' => $name, 'pass' => $pass, 'mail' => $mail, 'access' => '0', 'status' => 1, ); if (!drush_get_context('DRUSH_SIMULATE')) { if ($account = $userversion->create($new_user)) { return array($account->id() => $account->info()); } else { return drush_set_error("Could not create a new user account with the name " . $name . "."); } } } function drush_user_create_validate($name) { $userversion = drush_user_get_class(); if ($mail = drush_get_option('mail')) { if ($userversion->load_by_mail($mail)) { return drush_set_error(dt('There is already a user account with the email !mail', array('!mail' => $mail))); } } if ($userversion->load_by_name($name)) { return drush_set_error(dt('There is already a user account with the name !name', array('!name' => $name))); } } /** * Cancels a user account. */ function drush_user_cancel($inputs) { $userlist = new UserList($inputs); foreach ($userlist->accounts as $account) { if (drush_get_option('delete-content') && drush_drupal_major_version() >= 7) { drush_print(dt('All content created by !name will be deleted.', array('!name' => $account->getUsername()))); } if (drush_confirm('Cancel user account?: ')) { $account->cancel(); } } drush_log(dt('Cancelled user(s): !users', array('!users' => $userlist->names())),LogLevel::SUCCESS); } /** * Sets the password for the account with the given username */ function drush_user_password($inputs) { $userlist = new UserList($inputs); if (!drush_get_context('DRUSH_SIMULATE')) { $pass = drush_get_option('password'); // If no password has been provided, prompt for one. if (empty($pass)) { $pass = drush_prompt(dt('Password'), NULL, TRUE, TRUE); } foreach ($userlist->accounts as $account) { $userlist->each('password', array($pass)); } drush_log(dt('Changed password for !users', array('!users' => $userlist->names())), LogLevel::SUCCESS); } } /** * Displays a one time login link for the given user. */ function drush_user_login($inputs = '', $path = NULL) { $args = func_get_args(); // Redispatch if called against a remote-host so a browser is started on the // the *local* machine. $alias = drush_get_context('DRUSH_TARGET_SITE_ALIAS'); if (drush_sitealias_is_remote_site($alias)) { $return = drush_invoke_process($alias, 'user-login', $args, drush_redispatch_get_options(), array('integrate' => FALSE)); if ($return['error_status']) { return drush_set_error('Unable to execute user login.'); } else { // Prior versions of Drupal returned a string so cast to an array if needed. $links = is_string($return['object']) ? array($return['object']) : $return['object']; } } else { if (!drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { // Fail gracefully if unable to bootstrap Drupal. // drush_bootstrap() has already logged an error. return FALSE; } if (drush_get_option('uid', FALSE) || drush_get_option('name', FALSE) || drush_get_option('mail', FALSE)) { // If we only have a single argument and one of the user options is passed, // then we assume the argument is the path to open. if (count($args) == 1) { $path = $args[0]; } } // Try to load a user from provided options and arguments. try { $userlist = new UserList($inputs); } catch (UserListException $e) { // No user option or argument was passed, so we default to uid 1. $userlist = new UserList(1); } $links = $userlist->each('passResetUrl', array($path)); } $port = drush_get_option('redirect-port', FALSE); // There is almost always only one link so pick the first one for display and browser. // The full array is sent on backend calls. $first = current($links); drush_start_browser($first, FALSE, $port); drush_backend_set_result($links); return $first; } 'Enable profiling via XHProf', ); $command['sub-options']['xh']['xh-link'] = 'URL to your XHProf report site.'; $command['sub-options']['xh']['xh-path'] = 'Absolute path to the xhprof project.'; $command['sub-options']['xh']['xh-profile-builtins'] = 'Profile built-in PHP functions (defaults to TRUE).'; $command['sub-options']['xh']['xh-profile-cpu'] = 'Profile CPU (defaults to FALSE).'; $command['sub-options']['xh']['xh-profile-memory'] = 'Profile Memory (defaults to FALSE).'; } } } function xh_enabled() { if (drush_get_option('xh')) { if (!extension_loaded('xhprof')) { return drush_set_error('You must enable the xhprof PHP extension in your CLI PHP in order to profile.'); } if (!drush_get_option('xh-path', '')) { return drush_set_error('You must provide the xh-path option in your drushrc or on the CLI in order to profile.'); } return TRUE; } } /** * Determines flags for xhprof_enable based on drush options. */ function xh_flags() { $flags = 0; if (!drush_get_option('xh-profile-builtins', XH_PROFILE_BUILTINS)) { $flags |= XHPROF_FLAGS_NO_BUILTINS; } if (drush_get_option('xh-profile-cpu', XH_PROFILE_CPU)) { $flags |= XHPROF_FLAGS_CPU; } if (drush_get_option('xh-profile-memory', XH_PROFILE_MEMORY)) { $flags |= XHPROF_FLAGS_MEMORY; } return $flags; } /** * Implements hook_drush_init(). */ function xh_drush_init() { if (xh_enabled()) { if ($path = drush_get_option('xh-path', '')) { include_once $path . '/xhprof_lib/utils/xhprof_lib.php'; include_once $path . '/xhprof_lib/utils/xhprof_runs.php'; xhprof_enable(xh_flags()); } } } /** * Implements hook_drush_exit(). */ function xh_drush_exit() { if (xh_enabled()) { $args = func_get_args(); $namespace = 'Drush'; // variable_get('site_name', ''); $xhprof_data = xhprof_disable(); $xhprof_runs = new XHProfRuns_Default(); $run_id = $xhprof_runs->save_run($xhprof_data, $namespace); if ($url = xh_link($run_id)) { drush_log(dt('XHProf run saved. View report at !url', array('!url' => $url)), LogLevel::OK); } } } /** * Returns the XHProf link. */ function xh_link($run_id) { if ($xhprof_url = trim(drush_get_option('xh-link'))) { $namespace = 'Drush'; //variable_get('site_name', ''); return $xhprof_url . '/index.php?run=' . urlencode($run_id) . '&source=' . urlencode($namespace); } else { drush_log('Configure xh_link in order to see a link to the XHProf report for this request instead of this message.'); } } # Remote Operations on Drupal Sites via a Bastion Server Wikipedia defines a [bastion server](http://en.wikipedia.org/wiki/Bastion_host) as "a special purpose computer on a network specifically designed and configured to withstand attacks." For the purposes of this documentation, though, any server that you can ssh through to reach other servers will do. Using standard ssh and Drush techniques, it is possible to make a two-hop remote command look and act as if the destination machine is on the same network as the source machine. ## Recap of Remote Site Aliases Site aliases can refer to Drupal sites that are running on remote machines simply including 'remote-host' and 'remote-user' attributes: $aliases['internal'] = array( 'remote-host' => 'internal.company.com', 'remote-user' => 'wwwadmin', 'uri' => 'http://internal.company.com', 'root' => '/path/to/remote/drupal/root', ); With this alias defintion, you may use commands such as `drush @internal status`, `drush ssh @internal` and `drush rsync @internal @dev` to operate remotely on the internal machine. What if you cannot reach the server that site is on from your current network? Enter the bastion server. ## Setting up a Bastion server in .ssh/config If you have access to a server, bastion.company.com, which you can ssh to from the open internet, and if the bastion server can in turn reach the internal server, then it is possible to configure ssh to route all traffic to the internal server through the bastion. The .ssh configuration file would look something like this: In **.ssh/config:** Host internal.company.com ProxyCommand ssh user@bastion.company.com nc %h %p That is all that is necessary; however, if the dev machine you are configuring is a laptop that might sometimes be inside the company intranet, you might want to optimize this setup so that the bastion is not used when the internal server can be reached directly. You could do this by changing the contents of your .ssh/config file when your network settings change -- or you could use Drush. # Setting up a Bastion server via Drush configuration First, make sure that you do not have any configuration options for the internal machine in your .ssh/config file. The next step after that is to identify when you are connected to your company intranet. I like to determine this by using the `route` command to find my network gateway address, since this is always the same on my intranet, and unlikely to be encountered in other places. In **drushrc.php:** # Figure out if we are inside our company intranet by testing our gateway address against a known value exec("route -n | grep '^0\.0\.0\.0' | awk '{ print $2; }' 2> /dev/null", $output); if ($output[0] == '172.30.10.1') { drush_set_context('MY_INTRANET', TRUE); } After this code runs, the 'MY\_INTRANET' context will be set if our gateway IP address matches the expected value, and unset otherwise. We can make use of this in our alias files. In **aliases.drushrc.php:** if (drush_get_context('MY_INTRANET', FALSE) === FALSE) { $aliases['intranet-proxy'] = array( 'ssh-options' => ' -o "ProxyCommand ssh user@bastion.company.com nc %h %p"', ); } $aliases['internal-server'] = array( 'parent' => '@intranet-proxy', 'remote-host' => 'internal.company.com', 'remote-user' => 'wwwadmin', ); $aliases['internal'] = array( 'parent' => '@internal-server', 'uri' => 'http://internal.company.com', 'root' => '/path/to/remote/drupal/root', ); The 'parent' term of the internal-server alias record is ignored if the alias it references ('@intranet-proxy') is not defined; the result is that 'ssh-options' will only be defined when outside of the intranet, and the ssh ProxyCommand to the bastion server will only be included when it is needed. With this setup, you will be able to use your site alias '@internal' to remotely operate on your internal intranet Drupal site seemlessly, regardless of your location -- a handy trick indeed. The Drush Bootstrap Process =========================== When preparing to run a command, Drush works by "bootstrapping" the Drupal environment in very much the same way that is done during a normal page request from the web server, so most Drush commands run in the context of a fully-initialized website. For efficiency and convenience, some Drush commands can work without first bootstrapping a Drupal site, or by only partially bootstrapping a site. This is faster than a full bootstrap. It is also a matter of convenience, because some commands are useful even when you don't have a working Drupal site. For example, you can use Drush to download Drupal with `drush dl drupal`. This obviously does not require any bootstrapping to work. DRUSH\_BOOTSTRAP\_NONE ----------------------- Only run Drush _preflight_, without considering Drupal at all. Any code that operates on the Drush installation, and not specifically any Drupal directory, should bootstrap to this phase. DRUSH\_BOOTSTRAP\_DRUPAL\_ROOT ------------------------------ Set up and test for a valid Drupal root, either through the --root options, or evaluated based on the current working directory. Any code that interacts with an entire Drupal installation, and not a specific site on the Drupal installation should use this bootstrap phase. DRUSH\_BOOTSTRAP\_DRUPAL\_SITE ------------------------------ Set up a Drupal site directory and the correct environment variables to allow Drupal to find the configuration file. If no site is specified with the --uri options, Drush will assume the site is 'default', which mimics Drupal's behaviour. Note that it is necessary to specify a full URI, e.g. --uri=http://example.com, in order for certain Drush commands and Drupal modules to behave correctly. See the [example Config file](../examples/example.drushrc.php) for more information. Any code that needs to modify or interact with a specific Drupal site's settings.php file should bootstrap to this phase. DRUSH\_BOOTSTRAP\_DRUPAL\_CONFIGURATION --------------------------------------- Load the settings from the Drupal sites directory. This phase is analagous to the DRUPAL\_BOOTSTRAP\_CONFIGURATION bootstrap phase in Drupal itself, and this is also the first step where Drupal specific code is included. This phase is commonly used for code that interacts with the Drupal install API, as both install.php and update.php start at this phase. DRUSH\_BOOTSTRAP\_DRUPAL\_DATABASE ---------------------------------- Connect to the Drupal database using the database credentials loaded during the previous bootstrap phase. This phase is analogous to the DRUPAL\_BOOTSTRAP\_DATABASE bootstrap phase in Drupal. Any code that needs to interact with the Drupal database API needs to be bootstrapped to at least this phase. DRUSH\_BOOTSTRAP\_DRUPAL\_FULL ------------------------------ Fully initialize Drupal. This is analogous to the DRUPAL\_BOOTSTRAP\_FULL bootstrap phase in Drupal. Any code that interacts with the general Drupal API should be bootstrapped to this phase. DRUSH\_BOOTSTRAP\_DRUPAL\_LOGIN ------------------------------- Log in to the initialiazed Drupal site. This bootstrap phase is used after the site has been fully bootstrapped. This is the default bootstrap phase all commands will try to reach, unless otherwise specified. This phase will log you in to the drupal site with the username or user ID specified by the --user/ -u option(defaults to 0, anonymous). Use this bootstrap phase for your command if you need to have access to information for a specific user, such as listing nodes that might be different based on who is logged in. DRUSH\_BOOTSTRAP\_MAX --------------------- This is not an actual bootstrap phase. Commands that use DRUSH\_BOOTSTRAP\_MAX will cause Drush to bootstrap as far as possible, and then run the command regardless of the bootstrap phase that was reached. This is useful for Drush commands that work without a bootstrapped site, but that provide additional information or capabilities in the presence of a bootstrapped site. For example, `drush pm-releases modulename` works without a bootstrapped Drupal site, but will include the version number for the installed module if a Drupal site has been bootstrapped. Creating Custom Drush Commands ============================== Creating a new Drush command is very easy. Follow these simple steps: 1. Create a command file called COMMANDFILE.drush.inc 1. Implement the function COMMANDFILE\_drush\_command() 1. Implement the functions that your commands will call. These will usually be named drush\_COMMANDFILE\_COMMANDNAME(). For an example Drush command, see examples/sandwich.drush.inc. The steps for implementing your command are explained in more detail below. Create COMMANDFILE.drush.inc ---------------------------- The name of your Drush command is very important. It must end in ".drush.inc" to be recognized as a Drush command. The part of the filename that comes before the ".drush.inc" becomes the name of the commandfile. Optionally, the commandfile may be restricted to a particular version of Drupal by adding a ".dVERSION" after the name of the commandfile (e.g. ".d8.drush.inc") Your commandfile name is used by Drush to compose the names of the functions it will call, so choose wisely. The example Drush command, 'make-me-a-sandwich', is stored in the 'sandwich' commandfile, 'sandwich.Drush.inc'. You can find this file in the 'examples' directory in the Drush distribution. Drush searches for commandfiles in the following locations: - Folders listed in the 'include' option (see `drush topic docs-configuration`). - The system-wide Drush commands folder, e.g. /usr/share/drush/commands - The ".drush" folder in the user's HOME folder. - /drush and /sites/all/drush in the current Drupal installation - All enabled modules in the current Drupal installation - Folders and files containing other versions of Drush in their names will be \*skipped\* (e.g. devel.drush4.inc or drush4/devel.drush.inc). Names containing the current version of Drush (e.g. devel.drush5.inc) will be loaded. Note that modules in the current Drupal installation will only be considered if Drush has bootstrapped to at least the DRUSH\_BOOSTRAP\_SITE level. Usually, when working with a Drupal site, Drush will bootstrap to DRUSH\_BOOTSTRAP\_FULL; in this case, only the Drush commandfiles in enabled modules will be considered eligible for loading. If Drush only bootstraps to DRUSH\_BOOTSTRAP\_SITE, though, then all Drush commandfiles will be considered, whether the module is enabled or not. See `drush topic docs-bootstrap` for more information on bootstrapping. Implement COMMANDFILE\_drush\_command() --------------------------------------- The drush\_command hook is the most important part of the commandfile. It returns an array of items that define how your commands should be called, and how they work. Drush commands are very similar to the Drupal menu system. The elements that can appear in a Drush command definition are shown below. - **aliases**: Provides a list of shorter names for the command. For example, pm-download may also be called via `drush dl`. If the alias is used, Drush will substitute back in the primary command name, so pm-download will still be used to generate the command hook, etc. - **command-hook**: Change the name of the function Drush will call to execute the command from drush\_COMMANDFILE\_COMMANDNAME() to drush\_COMMANDFILE\_COMMANDHOOK(), where COMMANDNAME is the original name of the command, and COMMANDHOOK is the value of the 'command-hook' item. - **callback**: Name of function to invoke for this command. The callback function name \_must\_ begin with "drush\_commandfile\_", where commandfile is from the file "commandfile.drush.inc", which contains the commandfile\_drush\_command() function that returned this command. Note that the callback entry is optional; it is preferable to omit it, in which case drush\_invoke() will generate the hook function name. - **callback arguments**: An array of arguments to pass to the callback. The command line arguments, if any, will appear after the callback arguments in the function parameters. - **description**: Description of the command. - **arguments**: An array of arguments that are understood by the command. Used by `drush help` only. - **required-arguments**: Defaults to FALSE; TRUE if all of the arguments are required. Set to an integer count of required arguments if only some are required. - **options**: An array of options that are understood by the command. Any option that the command expects to be able to query via drush\_get\_option \_must\_ be listed in the options array. If it is not, users will get an error about an "Unknown option" when they try to specify the option on the command line. The value of each option may be either a simple string containing the option description, or an array containing the following information: - **description**: A description of the option. - **example-value**: An example value to show in help. - **value**: optional|required. - **required**: Indicates that an option must be provided. - **hidden**: The option is not shown in the help output (rare). - **allow-additional-options**: If TRUE, then the strict validation to see if options exist is skipped. Examples of where this is done includes the core-rsync command, which passes options along to the rsync shell command. This item may also contain a list of other commands that are invoked as subcommands (e.g. the pm-update command calls pm-updatecode and updatedb commands). When this is done, the options from the subcommand may be used on the commandline, and are also listed in the command's `help` output. Defaults to FALSE. - **examples**: An array of examples that are understood by the command. Used by `drush help` only. - **scope**: One of 'system', 'project', 'site'. Not currently used. - **bootstrap**: Drupal bootstrap level. More info at `drush topic docs-bootstrap`. Valid values are: - DRUSH\_BOOTSTRAP\_NONE - DRUSH\_BOOTSTRAP\_DRUPAL\_ROOT - DRUSH\_BOOTSTRAP\_DRUPAL\_SITE - DRUSH\_BOOTSTRAP\_DRUPAL\_CONFIGURATION - DRUSH\_BOOTSTRAP\_DRUPAL\_DATABASE - DRUSH\_BOOTSTRAP\_DRUPAL\_FULL - DRUSH\_BOOTSTRAP\_DRUPAL\_LOGIN (default) - DRUSH\_BOOTSTRAP\_MAX - **core**: Drupal major version required. Append a '+' to indicate 'and later versions.' - **drupal dependencies**: Drupal modules required for this command. - **drush dependencies**: Other Drush commandfiles required for this command. - **engines**: Provides a list of Drush engines to load with this command. The set of appropriate engines varies by command. - **outputformat**: One important engine is the 'outputformat' engine. This engine is responsible for formatting the structured data (usually an associative array) that a Drush command returns as its function result into a human-readable or machine-parsable string. Some of the options that may be used with output format engines are listed below; however, each specific output format type can take additional option items that control the way that the output is rendered. See the comment in the output format's implementation for information. The Drush core output format engines can be found in commands/core/outputformat. - **default**: The default type to render output as. If declared, the command should not print any output on its own, but instead should return a data structure (usually an associative array) that can be rendered by the output type selected. - **pipe-format**: When the command is executed in --pipe mode, the command output will be rendered by the format specified by the pipe-format item instead of the default format. Note that in either event, the user may specify the format to use via the --format command-line option. - **formatted-filter** and **pipe-filter**: Specifies a function callback that will be used to filter the command result. The filter is selected based on the type of output format object selected. Most output formatters are 'pipe' formatters, that produce machine-parsable output. A few formatters, such as 'table' and 'key-value' are 'formatted' filter types, that produce human-readable output. - **topics**: Provides a list of topic commands that are related in some way to this command. Used by `drush help`. - **topic**: Set to TRUE if this command is a topic, callable from the `drush docs-topics` command. - **category**: Set this to override the category in which your command is listed in help. The 'sandwich' drush\_command hook looks like this: function sandwich_drush_command() { $items = array(); $items['make-me-a-sandwich'] = array( 'description' => "Makes a delicious sandwich.", 'arguments' => array( 'filling' => 'The type of the sandwich (turkey, cheese, etc.)', ), 'options' => array( 'spreads' => 'Comma delimited list of spreads (e.g. mayonnaise, mustard)', ), 'examples' => array( 'drush mmas turkey --spreads=ketchup,mustard' => 'Make a terrible-tasting sandwich that is lacking in pickles.', ), 'aliases' => array('mmas'), 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, // No bootstrap at all. ); return $items; } Most of the items in the 'make-me-a-sandwich' command definition have no effect on execution, and are used only by `drush help`. The exceptions are 'aliases' (described above) and 'bootstrap'. As previously mentioned, `drush topic docs-bootstrap` explains the Drush bootstrapping process in detail. Implement drush\_COMMANDFILE\_COMMANDNAME() ------------------------------------------- The 'make-me-a-sandwich' command in sandwich.drush.inc is defined as follows: function drush_sandwich_make_me_a_sandwich($filling = 'ascii') { // implementation here ... } If a user runs `drush make-me-a-sandwich` with no command line arguments, then Drush will call drush\_sandwich\_make\_me\_a\_sandwich() with no function parameters; in this case, $filling will take on the provided default value, 'ascii'. (If there is no default value provided, then the variable will be NULL, and a warning will be printed.) Running `drush make-me-a-sandwich ham` will cause Drush to call drush\_sandwich\_make\_me\_a\_sandwich('ham'). In the same way, commands that take two command line arguments can simply define two functional parameters, and a command that takes a variable number of command line arguments can use the standard php function func\_get\_args() to get them all in an array for easy processing. It is also very easy to query the command options using the function drush\_get\_option(). For example, in the drush\_sandwich\_make\_me\_a\_sandwich() function, the --spreads option is retrieved as follows: $str_spreads = ''; if ($spreads = drush_get_option('spreads')) { $list = implode(' and ', explode(',', $spreads)); $str_spreads = ' with just a dash of ' . $list; } Note that Drush will actually call a sequence of functions before and after your Drush command function. One of these hooks is the "validate" hook. The 'sandwich' commandfile provides a validate hook for the 'make-me-a-sandwich' command: function drush_sandwich_make_me_a_sandwich_validate() { $name = posix_getpwuid(posix_geteuid()); if ($name['name'] !== 'root') { return drush_set_error('MAKE_IT_YOUSELF', dt('What? Make your own sandwich.')); } } The validate function should call drush\_set\_error() and return its result if the command cannot be validated for some reason. See `drush topic docs-policy` for more information on defining policy functions with validate hooks, and `drush topic docs-api` for information on how the command hook process works. Also, the list of defined drush error codes can be found in `drush topic docs-errorcodes`. To see the full implementation of the sample 'make-me-a-sandwich' command, see `drush topic docs-examplecommand`. # Exporting and Importing Configuration Drush provides commands to export, transfer, and import configuration files to and from a Drupal 8 site. Configuration can be altered by different methods in order to provide different behaviors in different environments; for example, a development server might be configured slightly differently than the production server. This document describes how to make simple value changes to configuration based on the environment, how to have a different set of enabled modules in different configurations without affecting your exported configuration values, and how to make more complex changes. ## Simple Value Changes It is not necessary to alter the configuration system values to make simple value changes to configuration variables, as this may be done by the [configuration override system](https://www.drupal.org/node/1928898). The configuration override system allows you to change configuration values for a given instance of a site (e.g. the development server) by setting configuration variables in the site's settings.php file. For example, to change the name of a local development site: ``` $config['system.site']['name'] = 'Local Install of Awesome Widgets, Inc.'; ``` Note that the configuration override system is a Drupal feature, not a Drush feature. It should be the preferred method for changing configuration values on a per-environment basis; however, it does not work for some things, such as enabling and disabling modules. For configuration changes not handled by the configuration override system, you can use configuration filters of the Config Filter module. ## Ignoring Development Modules Use the [Config Split](https://www.drupal.org/project/config_split) module to split off development configuration in a dedicated config directory.Drush Contexts ============== The drush contexts API acts as a storage mechanism for all options, arguments and configuration settings that are loaded into drush. This API also acts as an IPC mechanism between the different drush commands, and provides protection from accidentally overriding settings that are needed by other parts of the system. It also avoids the necessity to pass references through the command chain and allows the scripts to keep track of whether any settings have changed since the previous execution. This API defines several contexts that are used by default. Argument contexts ----------------- These contexts are used by Drush to store information on the command. They have their own access functions in the forms of drush\_set\_arguments(), drush\_get\_arguments(), drush\_set\_command(), drush\_get\_command(). - command : The drush command being executed. - arguments : Any additional arguments that were specified. Setting contexts ---------------- These contexts store options that have been passed to the drush.php script, either through the use of any of the config files, directly from the command line through --option='value' or through a JSON encoded string passed through the STDIN pipe. These contexts are accessible through the drush\_get\_option() and drush\_set\_option() functions. See drush\_context\_names() for a description of all of the contexts. Drush commands may also choose to save settings for a specific context to the matching configuration file through the drush\_save\_config() function. Available Setting contexts -------------------------- These contexts are evaluated in a certain order, and the highest priority value is returned by default from drush\_get\_option. This allows scripts to check whether an option was different before the current execution. Specified by the script itself : - process : Generated in the current process. - cli : Passed as --option=value to the command line. - stdin : Passed as a JSON encoded string through stdin. - alias : Defined in an alias record, and set in the alias context whenever that alias is used. - specific : Defined in a command-specific option record, and set in the command context whenever that command is used. Specified by config files : - custom : Loaded from the config file specified by --config or -c - site : Loaded from the drushrc.php file in the Drupal site directory. - drupal : Loaded from the drushrc.php file in the Drupal root directory. - user : Loaded from the drushrc.php file in the user's home directory. - drush : Loaded from the drushrc.php file in the $HOME/.drush directory. - system : Loaded from the drushrc.php file in the system's $PREFIX/etc/drush directory. - drush : Loaded from the drushrc.php file in the same directory as drush.php. Specified by the script, but has the lowest priority : - default : The script might provide some sensible defaults during init. Running Drupal cron tasks from Drush ==================================== Drupal cron tasks are often set up to be run via a wget call to cron.php; this same task can also be accomplished via the `drush cron` command, which circumvents the need to provide a webserver interface to cron. Quick start ---------- If you just want to get started quickly, here is a crontab entry that will run cron once every hour at ten minutes after the hour: 10 * * * * /usr/bin/env PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin COLUMNS=72 /usr/local/bin/drush --root=/path/to/your/drupalroot --uri=your.drupalsite.org --quiet cron You should set up crontab to run your cron tasks as the same user that runs the web server; for example, if you run your webserver as the user www-data: sudo crontab -u www-data -e You might need to edit the crontab entry shown above slightly for your particular setup; for example, if you have installed Drush to some directory other than /usr/local/drush, then you will need to adjust the path to drush appropriately. We'll break down the meaning of each section of the crontab entry in the documentation that continues below. Setting the schedule -------------------- See `man 5 crontab` for information on how to format the information in a crontab entry. In the example above, the schedule for the crontab is set by the string `10 * * * *`. These fields are the minute, hour, day of month, month and day of week; `*` means essentially 'all values', so `10 * * * *` will run any time the minute == 10 (once every hour). Setting the PATH ---------------- We use /usr/bin/env to run Drush so that we can set up some necessary environment variables that Drush needs to execute. By default, cron will run each command with an empty PATH, which would not work well with Drush. To find out what your PATH needs to be, just type: echo $PATH Take the value that is output and place it into your crontab entry in the place of the one shown above. You can remove any entry that is known to not be of interest to Drush (e.g. /usr/games), or is only useful in a graphic environment (e.g. /usr/X11/bin). Setting COLUMNS --------------- When running Drush in a terminal, the number of columns will be automatically determined by Drush by way of the tput command, which queries the active terminal to determine what the width of the screen is. When running Drush from cron, there will not be any terminal set, and the call to tput will produce an error message. Spurrious error messages are undesirable, as cron is often configured to send email whenever any output is produced, so it is important to make an effort to insure that successful runs of cron complete with no output. In some cases, Drush is smart enough to recognize that there is no terminal -- if the terminal value is empty or "dumb", for example. However, there are some "non-terminal" values that Drush does not recognize, such as "unknown." If you manually set `COLUMNS`, then Drush will repect your setting and will not attempt to call tput. Using --quiet ------------- By default, Drush will print a success message when the run of cron is completed. The --quiet flag will suppress these and other progress messages, again avoiding an unnecessary email message. Specifying the Drupal site to run --------------------------------- There are many ways to tell Drush which Drupal site to select for the active command, and any may be used here. The example uses the --root and --uri flags, but you could also use an alias record. The _examples_ folder contains example files which you may copy and edit as needed. Read the documentation right in the file. If you see an opportunity to improve the file, please submit a pull request. * [drush.wrapper](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/drush.wrapper). A handy launcher script which calls the Drush located in vendor/bin/drush and can add options like --local, --root, etc. * [example.aliases.drushrc.php](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/example.aliases.drushrc.php). Example site alias definitions. * [example.bashrc](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/example.bashrc). Enhance your shell with lots of Drush niceties including bash completion. * [example.drush.ini](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/example.drush.ini). Configure your PHP just for Drush requests. * [example.drushrc.php](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/example.drushrc.php). A Drush configuration file. * [example.make](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/example.make). An ini style make file. * [example.make.yml](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/example.make.yml). A YML make file. * [example.prompt.sh](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/example.prompt.sh). Displays Git repository and Drush alias status in your prompt. * [git-bisect.example.sh](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/git-bisect.example.sh). Spelunking through Drush's git history with bisect. * [helloworld.script](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/helloworld.script). An example Drush script. * [pm_update.drush.inc](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/pm_update.drush.inc). Restore sqlsrv driver after core update. * [policy.drush.inc](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/policy.drush.inc). A policy file can disallow prohibited commands/options etc. * [sandwich.drush.inc](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/sandwich.drush.inc). A fun example command inspired by a famous XKCD comic. * [sync_via_http.drush.inc](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/sync_via_http.drush.inc). sql-sync modification that transfers via http instead of rsync. * [sync_enable.drush.inc](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/sync_enable.drush.inc). Automatically enable modules after a sql-sync. * [xkcd.drush.inc](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/xkcd.drush.inc). A fun example command that browses XKCD comics. Drush is a command line shell and Unix scripting interface for Drupal. Drush core ships with lots of useful commands for interacting with code like modules/themes/profiles. Similarly, it runs update.php, executes sql queries and DB migrations, and misc utilities like run cron or clear cache. Drush can be extended by [3rd party commandfiles](https://www.drupal.org/project/project_module?f[2]=im_vid_3%3A4654). [![Latest Stable Version](https://poser.pugx.org/drush/drush/v/stable.png)](https://packagist.org/packages/drush/drush) [![Total Downloads](https://poser.pugx.org/drush/drush/downloads.png)](https://packagist.org/packages/drush/drush) [![Latest Unstable Version](https://poser.pugx.org/drush/drush/v/unstable.png)](https://packagist.org/packages/drush/drush) [![License](https://poser.pugx.org/drush/drush/license.png)](https://packagist.org/packages/drush/drush) Resources ----------- * [Install documentation](http://docs.drush.org/en/8.x/install/) * [General documentation](http://docs.drush.org) * [API Documentation](http://api.drush.org) * [Drush Commands](http://drushcommands.com) * Subscribe [this atom feed](https://github.com/drush-ops/drush/releases.atom) to receive notification on new releases. Also, [Version eye](https://www.versioneye.com/). * [Drush packages available via Composer](http://packages.drush.org) * [A list of modules that include Drush integration](https://www.drupal.org/project/project_module?f[2]=im_vid_3%3A4654&solrsort=ds_project_latest_release+desc) * Drush comes with a [full test suite](https://github.com/drush-ops/drush/blob/8.x/tests/README.md) powered by [PHPUnit](https://github.com/sebastianbergmann/phpunit). Each commit gets tested by the awesome [Travis.ci continuous integration service](https://travis-ci.org/drush-ops/drush). Support ----------- Please take a moment to review the rest of the information in this file before pursuing one of the support options below. * Post support requests to [Drupal Answers](http://drupal.stackexchange.com/questions/tagged/drush). * Bug reports and feature requests should be reported in the [GitHub Drush Issue Queue](https://github.com/drush-ops/drush/issues). * Use pull requests (PRs) to contribute to Drush. * It is still possible to search the old issue queue on Drupal.org for [fixed bugs](https://drupal.org/project/issues/search/drush?status%5B%5D=7&categories%5B%5D=bug), [unmigrated issues](https://drupal.org/project/issues/search/drush?status%5B%5D=5&issue_tags=needs+migration), [unmigrated bugs](https://drupal.org/project/issues/search/drush?status%5B%5D=5&categories%5B%5D=bug&issue_tags=needs+migration), and so on. FAQ ------ ##### What does the name Drush mean? The Drupal Shell. ##### How do I pronounce Drush? Some people pronounce the dru with a long u like Drupal. Fidelity points go to them, but they are in the minority. Most pronounce Drush so that it rhymes with hush, rush, flush, etc. This is the preferred pronunciation. ##### Does Drush have unit tests? Drush has an excellent suite of unit tests. See [tests/README.md](https://github.com/drush-ops/drush/blob/8.x/tests/README.md) for more information. Credits ----------- * Originally developed by [Arto Bendiken](http://bendiken.net) for Drupal 4.7. * Redesigned by [Franz Heinzmann](http://unbiskant.org) in May 2007 for Drupal 5. * Maintained by [Moshe Weitzman](http://drupal.org/moshe) with much help from Owen Barton, greg.1.anderson, jonhattan, Mark Sonnabaum, Jonathan Hedstrom and [Christopher Gervais](http://drupal.org/u/ergonlogic). Install a global Drush via Composer ------------------ Follow the instructions below, or [watch a video by Drupalize.me](https://youtu.be/eAtDaD8xz0Q). 1. [Install Composer globally](https://getcomposer.org/doc/00-intro.md#globally). 1. Add composer's `bin` directory to the system path by placing `export PATH="$HOME/.composer/vendor/bin:$PATH"` into your ~/.bash_profile (Mac OS users) or into your ~/.bashrc (Linux users). 1. Install latest stable Drush: `composer global require drush/drush`. 1. Verify that Drush works: `drush status` #### Notes * Update to latest release (per your specification in ~/.composer/composer.json): `composer global update` * Install a specific version of Drush: # Install a specific version of Drush, e.g. Drush 7.1.0 composer global require drush/drush:7.1.0 # Install 8.x branch as a git clone. Great for contributing back to Drush project. composer global require drush/drush:8.x-dev --prefer-source * Alternate way to install for all users via Composer: COMPOSER_HOME=/opt/drush COMPOSER_BIN_DIR=/usr/local/bin COMPOSER_VENDOR_DIR=/opt/drush/7 composer require drush/drush:7 * [Documentation for composer's require command.](http://getcomposer.org/doc/03-cli.md#require) * Uninstall with : `composer global remove drush/drush` Windows ------------ Drush on Windows is experimental, since Drush's test suite is not running there ([help wanted](https://github.com/drush-ops/drush/issues/1612)). * [Acquia Dev Desktop](https://www.acquia.com/downloads) is excellent, and includes Drush. See the terminal icon after setting up a web site. * Or consider running Linux/OSX via Virtualbox. [Drupal VM](http://www.drupalvm.com/) and [Vlad](https://github.com/hashbangcode/vlad) are popular.* These Windows packages include Drush and its dependencies (including MSys). * [7.0.0 (stable)](https://github.com/drush-ops/drush/releases/download/7.0.0/windows-7.0.0.zip). * [6.6.0](https://github.com/drush-ops/drush/releases/download/6.6.0/windows-6.6.0.zip). * [6.0](https://github.com/drush-ops/drush/releases/download/6.0.0/Drush-6.0-2013-08-28-Installer-v1.0.21.msi). * Or install LAMP on your own, and run Drush via [Git's shell](https://git-for-windows.github.io/), in order to insure that [all depedencies](https://github.com/acquia/DevDesktopCommon/tree/8.x/bintools-win/msys/bin) are available. * When creating site aliases for Windows remote machines, pay particular attention to information presented in the example.aliases.drushrc.php file, especially when setting values for 'remote-host' and 'os', as these are very important when running Drush rsync and Drush sql-sync commands. Install/Upgrade a global Drush --------------- ```bash # Download latest stable release using the code below or browse to github.com/drush-ops/drush/releases. wget http://files.drush.org/drush.phar # Or use our upcoming release: wget http://files.drush.org/drush-unstable.phar # Test your install. php drush.phar core-status # Rename to `drush` instead of `php drush.phar`. Destination can be anywhere on $PATH. chmod +x drush.phar sudo mv drush.phar /usr/local/bin/drush # Optional. Enrich the bash startup file with completion and aliases. drush init ``` * MAMP users, and anyone wishing to launch a non-default PHP, needs to [edit ~/.bashrc so that the right PHP is in your $PATH](http://stackoverflow.com/questions/4145667/how-to-override-the-path-of-php-to-use-the-mamp-path/10653443#10653443). * We have documented [alternative ways to install](http://docs.drush.org/en/8.x/install-alternative/), including [Windows](http://docs.drush.org/en/8.x/install-alternative/#windows). * If you need to pass custom php.ini values, run `php -d foo=bar drush.phar --php-options=foo=bar` * Your shell now has [useful bash aliases and tab completion for command names, site aliases, options, and arguments](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/example.bashrc). * A [drushrc.php](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/example.drushrc.php) has been copied to ~/.drush above. Customize it to save typing and standardize options for commands. * Upgrade using this same procedure. Install a site-local Drush ----------------- In addition to the global Drush, it is recommended that Drupal 8 sites be [built using Composer, with Drush listed as a dependency](https://github.com/drupal-composer/drupal-project). 1. When you run `drush`, the global Drush is called first and then hands execution to the site-local Drush. This gives you the convenience of running `drush` without specifying the full path to the executable, without sacrificing the safety provided by a site-local Drush. 2. Optional: Copy the [examples/drush.wrapper](https://github.com/drush-ops/drush/blob/8.x/examples/drush.wrapper) file to your project root and modify to taste. This is a handy launcher script; add --local here to turn off all global configuration locations, and maintain consistency over configuration/aliases/commandfiles for your team. 3. Note that if you have multiple Drupal sites on your system, it is possible to use a different version of Drush with each one. Drupal Compatibility ----------------- Drush Version | Drush Branch | PHP | Compatible Drupal versions | Code Status ------------- | --------- | --- | -------------------------- | ----------- Drush 9 | [master](https://travis-ci.org/drush-ops/drush) | 5.5+ | D7, D8 | Drush 8 | [8.x](https://travis-ci.org/drush-ops/drush) | 5.4.5+ | D6, D7, D8 | Drush 7 | [7.x](https://travis-ci.org/drush-ops/drush) | 5.3.0+ | D6, D7 | Drush 6 | [6.x](https://travis-ci.org/drush-ops/drush) | 5.3.0+ | D6, D7 | Unsupported Drush 5 | [5.x](https://travis-ci.org/drush-ops/drush) | 5.2.0+ | D6, D7 | Unsupported Drush make ---------- Drush make is an extension to drush that can create a ready-to-use drupal site, pulling sources from various locations. It does this by parsing a flat text file (similar to a drupal `.info` file) and downloading the sources it describes. In practical terms, this means that it is possible to distribute a complicated Drupal distribution as a single text file. Among Drush make's capabilities are: - Downloading Drupal core, as well as contrib modules from drupal.org. - Checking code out from SVN, git, and bzr repositories. - Getting plain `.tar.gz` and `.zip` files (particularly useful for libraries that can not be distributed directly with drupal core or modules). - Fetching and applying patches. - Fetching modules, themes, and installation profiles, but also external libraries. Usage ----- The `drush make` command can be executed from a path within a Drupal codebase or independent of any Drupal sites entirely. See the examples below for instances where `drush make` can be used within an existing Drupal site. drush make [-options] [filename.make] [build path] The `.make` file format ----------------------- Each makefile is a plain text file that adheres to YAML syntax. See the included `examples/example.make.yml` for an example of a working makefile. The older Drupal `.info` INI format is also supported. See `examples/example.make` for a working example. ### Core version The make file always begins by specifying the core version of Drupal for which each package must be compatible. Example: core: 7.x ### API version The make file must specify which Drush Make API version it uses. This version of Drush Make uses API version `2` api: 2 ### Projects An array of the projects (e.g. modules, themes, libraries, and drupal) to be retrieved. Each project name can be specified as a single string value. If further options need to be provided for a project, the project should be specified as the key. **Project with no further options:** projects: - drupal **Project using options (see below):** projects: drupal: version: "7.33" Do not use both types of declarations for a single project in your makefile. ### Project options - `version` Specifies the version of the project to retrieve. This can be as loose as the major branch number or as specific as a particular point release. projects: views: # Picks the latest release. version: ~ projects: views: version: "2.8" projects: views: version: "3.0-alpha2" # Shorthand syntax for versions if no other options are to be specified projects: views: "3.0-alpha2" Note that version numbers should be enclosed in quotes to ensure they are interpreted correctly by the YAML parser. - `patch` One or more patches to apply to this project. An array of patches, each specified as a URL or local path relative to the makefile. projects: calendar: patch: rfc-fixes: url: "http://drupal.org/files/issues/cal-760316-rfc-fixes-2.diff" md5: "e4876228f449cb0c37ffa0f2142" adminrole: # shorthand syntax if no md5 checksum is specified patch: - "http://drupal.org/files/issues/adminrole_exceptions.patch" - "http://drupal.org/files/issues/adminrole-213212-01.patch" - "adminrole-customizations.patch" - `subdir` Place a project within a subdirectory of the `--contrib-destination` specified. In the example below, `cck` will be placed in `sites/all/modules/contrib` instead of the default `sites/all/modules`. projects: cck: subdir: "contrib" - `location` URL of an alternate project update XML server to use. Allows project XML data to be retrieved from sites other than `updates.drupal.org`. projects: tao: location: "http://code.developmentseed.com/fserver" - `type` The project type. Must be provided if an update XML source is not specified and/or using version control or direct retrieval for a project. May be one of the following values: core, module, profile, theme. projects: mytheme: type: "theme" - `directory_name` Provide an alternative directory name for this project. By default, the project name is used. projects: mytheme: directory_name: "yourtheme" - `l10n_path` Specific URL (can include tokens) to a translation. Allows translations to be retrieved from l10n servers other than `localize.drupal.org`. projects: mytheme: l10n_path: "http://myl10nserver.com/files/translations/%project-%core-%version-%language.po" - `l10n_url` URL to an l10n server XML info file. Allows translations to be retrieved from l10n servers other than `localize.drupal.org`. projects: mytheme: l10n_url: "http://myl10nserver.com/l10n_server.xml" - `overwrite` Allows the project to be installed in a directory that is not empty. If not specified this is treated as FALSE, Drush make sets an error when the directory is not empty. If specified TRUE, Drush make will continue and use the existing directory. Useful when adding extra files and folders to existing folders in libraries or module extensions. projects: myproject: overwrite: TRUE - `translations` Retrieve translations for the specified language, if available, for all projects. translations: - es - fr - `do_recursion` Recursively build an included makefile. Defaults to 'true'. do_recursion: false - `variant` Which type of tarball to download for profiles. Valid options include: - 'full': complete distro including Drupal core, e.g. `distro_name-core.tar.gz` - 'projects': the fully built profile, projects defined drupal-org.make, etc., e.g. `distro_name-no-core.tar.gz` - 'profile-only' (just the bare profile, e.g. `distro_name.tar.gz`). Defaults to 'profile-only'. When using 'projects', `do_recursion: false` will be necessary to avoid recursively making any makefiles included in the profile. variant: projects ### Project download options Use an alternative download method instead of retrieval through update XML. If no download type is specified, make defaults the type to `git`. Additionally, if no url is specified, make defaults to use Drupal.org. The following methods are available: - `download[type] = file` Retrieve a project as a direct download. Options: `url` - the URL of the file. Required. The URL can also be a path to a local file either using the bare path or the file:// protocol. The path may be absolute or relative to the makefile. `md5`, `sha1`, `sha256`, or `sha512` - one or more checksums for the file. Optional. `request_type` - the request type - get or post. post depends on http://drupal.org/project/make_post. Optional. `data` - The post data to be submitted with the request. Should be a valid URL query string. Requires http://drupal.org/project/make_post. Optional. `filename` - What to name the file, if it's not an archive. Optional. `subtree` - if the download is an archive, only this subtree within the archive will be copied to the target destination. Optional. - `download[type] = copy` Copies a project from a local folder. Options: `url` - the URL of the folder. Required. The URL must be a path to a local folder either using the bare path or the file:// protocol. The path may be absolute or relative to the makefile. projects[example][type] = "profile" projects[example][download][type] = "copy" projects[example][download][url] = "file://./example" - `download[type] = bzr` Use a bazaar repository as the source for this project. Options: `url` - the URL of the repository. Required. `working-copy` - If true, the checked out source will be kept as a working copy rather than exported as standalone files - `download[type] = git` Use a git repository as the source for this project. Options: `url` - the URL of the repository. Required. `branch` - the branch to be checked out. Optional. `revision` - a specific revision identified by commit to check out. Optional. Note that it is recommended on use `branch` in combination with `revision` if relying on the .info file rewriting. `tag` - the tag to be checked out. Optional. projects[mytheme][download][type] = "git" projects[mytheme][download][url] = "git://github.com/jane_doe/mytheme.git" `refspec` - the git reference to fetch and checkout. Optional. If this is set, it will have priority over tag, revision and branch options. `working-copy` - If true, the checked out source will be kept as a working copy rather than exported as standalone files - `download[type] = svn` Use an SVN repository as the source for this project. Options: `url` - the URL of the repository. Required. `interactive` - whether to prompt the user for authentication credentials when using a private repository. Allows username and/or password options to be omitted. Optional. `username` - the username to use when retrieving an SVN project as a working copy or from a private repository. Optional. `password` - the password to use when retrieving an SVN project as a working copy or from a private repository. Optional. projects: mytheme: download: type: "svn" url: "http://example.com/svnrepo/cool-theme/" `working-copy` - If true, the checked out source will be kept as a working copy rather than exported as standalone files Shorthand for `download[url]` available for all download types: projects: mytheme: download: "git://github.com/jane_doe/mytheme.git" is equivalent to: projects: mytheme: download: url: "git://github.com/jane_doe/mytheme.git" ### Libraries An array of non-Drupal-specific libraries to be retrieved (e.g. js, PHP or other Drupal-agnostic components). Each library should be specified as the key of an array of options in the libraries array. **Example:** libraries: jquery_ui: download: type: "file" url: "http://jquery- ui.googlecode.com/files/jquery.ui-1.6.zip" md5: "c177d38bc7af59d696b2efd7dda5c605" ### Library options Libraries share the `download`, `subdir`, and `directory_name` options with projects. Additionally, they may specify a destination: - `destination` The target path to which this library should be moved. The path is relative to that specified by the `--contrib-destination` option. By default, libraries are placed in the `libraries` directory. libraries: jquery_ui: destination: "modules/contrib/jquery_ui" ### Includes An array of makefiles to include. Each include may be a local relative path to the include makefile directory, a direct URL to the makefile, or from a git repository. Includes are appended in order with the source makefile appended last. As a result, values in the source makefile take precedence over those in includes. Use `overrides` for the reverse order of precedence. **Example:** includes: # Includes a file in the same directory. - "example.make" # Includes a file with a relative path. - "../example_relative/example_relative.make" # A remote-hosted file. - "http://www.example.com/remote.make" # A file on a git repository. - makefile: "example_dir/example.make" download: type: "git" url: "git@github.com:organisation/repository.git" # Branch could be tag or revision, it relies on the standard Drush git download feature. branch: "master" The `--includes` option is available for most make commands, and allows makefiles to be included at build-time. **Example:** # Build from a production makefile, but add development and test projects. $ drush make production.make --includes=dev.make,test.make ### Overrides Similar to `includes`, `overrides` will include content from other makefiles. However, the order of precedence is reversed. That is, they override the keys/values of the source makefile. The `--overrides` option is available for most make commands, and allows overrides to be included at build-time. **Example:** #production.make.yml: api: 2 core: 8.x includes: - core.make - contrib.make projects: custom_feature_A: type: module download: branch: production type: git url: http://github.com/example/custom_feature_A.git custom_feature_B: type: module download: branch: production type: git url: http://github.com/example/custom_feature_B.git # Build production code-base. $ drush make production.make.yml #testing.make projects: custom_feature_A: download: branch: dev/bug_fix custom_feature_B: download: branch: feature/new_feature # Build production code-base using development/feature branches for custom code. $ drush make /path/to/production.make --overrides=http://url/of/testing.make ### Defaults If all projects or libraries have identical settings for a given attribute, the `defaults` array can be used to specify these, rather than specifying the attribute for each project. **Example:** # Specify common subdir of "contrib" defaults: projects: subdir: "contrib" # Projects that don't specify subdir will go to the 'contrib' directory. projects: views: version: "3.3" # Override a default value. devel: subdir: "development" ### Overriding properties Makefiles which include others may override the included makefiles properties. Properties in the includer takes precedence over the includee. **Example:** `base.make` core: "6.x" views: subdir: "contrib" cck: subdir: "contrib" `extender.make` includes: - "base.make" projects: views: # This line overrides the included makefile's 'subdir' option subdir: "patched" # These lines overrides the included makefile, switching the download type # to a git clone. type: "module" download: type: "git" url: "http://git.drupal.org/project/views.git" A project or library entry of an included makefile can be removed entirely by setting the corresponding key to NULL: # This line removes CCK entirely which was defined in base.make cck: ~ Recursion --------- If a project that is part of a build contains a `.make.yml` itself, Drush make will automatically parse it and recurse into a derivative build. For example, a full build tree may look something like this: Drush make distro.make distro distro.make FOUND - Drupal core - Foo bar install profile + foobar.make.yml FOUND - CCK - Token - Module x + x.make FOUND - External library x.js - Views - etc. Recursion can be used to nest an install profile build in a Drupal site, easily build multiple install profiles on the same site, fetch library dependencies for a given module, or bundle a set of module and its dependencies together. For Drush Make to recognize a makefile embedded within a project, the makefile itself must have the same name as the project. For instance, the makefile embedded within the managingnews profile must be called "managingnews.make". If no makefile matching the project's name is found, Drush Make will look for a "drupal-org.make.yml" makefile instead. The file must be in the project's root directory. Subdirectories will be ignored. **Build a full Drupal site with the Managing News install profile:** core: 6.x api: 2 projects: - drupal - managingnews ** Use a distribution as core ** core: 7.x api: 2 projects: commerce_kickstart: type: "core" version: "7.x-1.19" This behavior can be overridden globally using the `--no-recursion` option, or on a project-by-project basis by setting the `do_recursion` project parameter to 'false' in a makefile: core: 7.x api: 2 projects: drupal: type: core hostmaster: type: profile do_recursion: false Testing ------- Drush make also comes with testing capabilities, designed to test Drush make itself. Writing a new test is extremely simple. The process is as follows: 1. Figure out what you want to test. Write a makefile that will test this out. You can refer to existing test makefiles for examples. These are located in `DRUSH/tests/makefiles`. 2. Drush make your makefile, and use the --md5 option. You may also use other options, but be sure to take note of which ones for step 4. 3. Verify that the result you got was in fact what you expected. If so, continue. If not, tweak it and re-run step 2 until it's what you expected. 4. Using the md5 hash that was spit out from step 2, make a new entry in the tests clase (DRUSH/tests/makeTest.php), following the example below. 'machine-readable-name' => array( 'name' => 'Human readable name', 'makefile' => 'tests/yourtest.make', 'messages' => array( 'Build hash: f68e6510-your-hash-e04fbb4ed', ), 'options' => array('any' => TRUE, 'other' => TRUE, 'options' => TRUE), ), 5. Test! Run Drush test suite (see DRUSH/tests/README.md). To just run the make tests: `./unish.sh --filter=makeMake .` You can check for any messages you want in the message array, but the most basic tests would just check the build hash. Generate -------- Drush make has a primitive makefile generation capability. To use it, simply change your directory to the Drupal installation from which you would like to generate the file, and run the following command: `drush generate-makefile /path/to/make-file.make` This will generate a basic makefile. If you have code from other repositories, the makefile will not complete - you'll have to fill in some information before it is fully functional. Maintainers ----------- - Jonathan Hedstrom ([jhedstrom](https://www.drupal.org/u/jhedstrom)) - Christopher Gervais ([ergonlogic](http://drupal.org/u/ergonlogic)) - [The rest of the Drush maintainers](https://github.com/drush-ops/drush/graphs/contributors) Original Author --------------- [Dmitri Gaskin (dmitrig01)](https://twitter.com/dmitrig01) Drush Output Formats ==================== Many Drush commands produce output that may be rendered in a variety of different ways using a pluggable formatting system. Drush commands that support output formats will show a --format option in their help text. The available formats are also listed in the help text, along with the default value for the format option. The list of formats shown is abbreviated; to see the complete list of available formats, run the help command with the --verbose option. The --pipe option is a quick, consistent way to get machine readable output from a command, in whatever way the command author thought was helpful. The --pipe option is equivalent to using --format=`` The pipe format will be shown in the options section of the command help, under the --pipe option. For historic reasons, --pipe also hides all log messages. To best understand how the various Drush output formatters work, it is best to first look at the output of the command using the 'var\_export' format. This will show the result of the command using the exact structure that was built by the command, without any reformatting. This is the standard format for the Drush command. Different formatters will take this information and present it in different ways. Global Options -------------- - --list-separator: Specify how elements in a list should be separated. In lists of lists, this applies to the elements in the inner lists. - --line-separator: In nested lists of lists, specify how the outer lists ("lines") should be separated. Output Formats -------------- A list of available formats, and their affect on the output of certain Drush commands, is shown below. You can then use an interactive PHP REPL with your bootstrapped site (remote or local). It’s a Drupal code playground. You can do quick code experimentation, grab some data, or run Drush commands. This can also help with debugging certain issues. See [this blog post](http://blog.damiankloip.net/2015/drush-php) for an introduction. Run `help` for a list of commands. Drush Shell Aliases =================== A Drush shell alias is a shortcut to any Drush command or any shell command. Drush shell aliases are very similar to [git aliases](https://git.wiki.kernel.org/index.php/Aliases\#Advanced). A shell alias is defined in a Drush configuration file called drushrc.php. See `drush topic docs-configuration`. There are two kinds of shell aliases: an alias whose value begins with a '!' will execute the rest of the line as bash commands. Aliases that do not start with a '!' will be interpreted as Drush commands. $options['shell-aliases']['pull'] = '!git pull'; $options['shell-aliases']['noncore'] = 'pm-list --no-core'; With the above two aliases defined, `drush pull` will then be equivalent to `git pull`, and `drush noncore` will be equivalent to `drush pm-list --no-core`. Shell Alias Replacements ------------------------ Shell aliases are even more powerful when combined with shell alias replacements and site aliases. Shell alias replacements take the form of {{sitealias-item}} or {{%pathalias-item}}, and also the special {{@target}}, which is replaced with the name of the site alias used, or '@none' if none was used. For example, given the following site alias: $aliases['dev'] = array ( 'root' => '/path/to/drupal', 'uri' => 'http://example.com', '#live' => '@acme.live', ); The alias below can be used for all your projects to fetch the database and files from the client's live site via `drush @dev pull-data`. Note that these aliases assume that the alias used defines an item named '\#live' (as shown in the above alias). $options['shell-aliases'] = array( 'pull-data' => '!drush sql-sync {{#live}} {{@target}} && drush rsync {{#live}}:%files {{@target}}:%files' ); If the user does not use these shell aliases with any site alias, then an error will be returned and the script will not run. These aliases with replacements can be used to quickly run combinations of drush sql-sync and rsync commands on the "standard" source or target site, reducing the risk of typos that might send information in the wrong direction or to the wrong site. Drush Shell Scripts =================== A drush shell script is any Unix shell script file that has its "execute" bit set (i.e., via `chmod +x myscript.drush`) and that begins with a specific line: #!/usr/bin/env drush or #!/full/path/to/drush The former is the usual form, and is more convenient in that it will allow you to run the script regardless of where drush has been installed on your system, as long as it appears in your PATH. The later form allows you to specify the drush command add options to use, as in: #!/full/path/to/drush php-script --some-option Adding specific options is important only in certain cases, described later; it is usually not necessary. Drush scripts do not need to be named "\*.drush" or "\*.script"; they can be named anything at all. To run them, make sure they are executable (`chmod +x helloworld.script`) and then run them from the shell like any other script. There are two big advantages to drush scripts over bash scripts: - They are written in php - drush can bootstrap your Drupal site before running your script. To bootstrap a Drupal site, provide an alias to the site to bootstrap as the first commandline argument. For example: $ helloworld.script @dev a b c If the first argument is a valid site alias, drush will remove it from the arument list and bootstrap that site, then run your script. The script itself will not see @dev on its argument list. If you do not want drush to remove the first site alias from your scripts argument list (e.g. if your script wishes to syncronise two sites, specified by the first two arguments, and does not want to bootstrap either of those two sites), then fully specify the drush command (php-script) and options to use, as shown above. By default, if the drush command is not specified, drush will provide the following default line: #!/full/path/to/drush php-script --bootstrap-to-first-arg It is the option --bootstrap-to-first-arg that causes drush to pull off the first argument and bootstrap it. The way to get rid of that option is to specify the php-script line to run, and leave it off, like so: #!/full/path/to/drush php-script Note that 'php-script' is the only built-in drush command that makes sense to put on the "shebang" ("\#!" is pronounced "shebang") line. However, if you wanted to, you could implement your own custom version of php-script (e.g. to preprocess the script input, perhaps), and specify that command on the shebang line. Drush scripts can access their arguments via the drush\_shift() function: while ($arg = drush_shift()) { drush_print($arg); } Options are available via drush\_get\_option('option-name'). The directory where the script was launched is available via drush_cwd() See the example drush script in `drush topic docs-examplescript`, and the list of drush error codes in `drush topic docs-errorcodes`. Strict Option Handling ====================== Some Drush commands use strict option handling; these commands require that all Drush global option appear on the command line before the Drush command name. One example of this is the core-rsync command: drush --simulate core-rsync -v @site1 @site2 The --simulate option is a Drush global option that causes Drush to print out what it would do if the command is executed, without actually taking any action. Commands such as core-rsync that use strict option handling require that --simulate, if used, must appear before the command name. Most Drush commands allow the --simulate to be placed anywhere, such as at the end of the command line. The -v option above is an rsync option. In this usage, it will cause the rsync command to run in verbose mode. It will not cause Drush to run in verbose mode, though, because it appears after the core-rsync command name. Most Drush commands would be run in verbose mode if a -v option appeared in the same location. The advantage of strict option handling is that it allows Drush to pass options and arguments through to a shell command. Some shell commands, such as rsync and ssh, either have options that cannot be represented in Drush. For example, rsync allows the --exclude option to appear multiple times on the command line, but Drush only allows one instance of an option at a time for most Drush commands. Strict option handling overcomes this limitation, plus possible conflict between Drush options and shell command options with the same name, at the cost of greater restriction on where global options can be placed. Usage ----------- Drush can be run in your shell by typing "drush" from within any Drupal root directory. $ drush [options] [argument1] [argument2] Use the 'help' command to get a list of available options and commands: $ drush help For even more documentation, use the 'topic' command: $ drush topic Options ----------- For multisite installations, use the --uri option to target a particular site. If you are outside the Drupal web root, you might need to use the --root, --uri or other command line options just for Drush to work. $ drush --uri=http://example.com pm-updatecode If you wish to be able to select your Drupal site implicitly from the current working directory without using the --uri option, but you need your base_url to be set correctly, you may force it by setting the uri in a drushrc.php file located in the same directory as your settings.php file. ``` $options['uri'] = "http://example.com"; ``` Site Aliases ------------ Drush lets you run commands on a remote server, or even on a set of remote servers. Once defined, aliases can be referenced with the @ nomenclature, i.e. ```bash # Run pending updates on staging site. $ drush @staging updatedb # Synchronize staging files to production $ drush rsync @staging:%files/ @live:%files # Synchronize database from production to dev, excluding the cache table $ drush sql-sync --structure-tables-key=custom @live @dev ``` See [example.aliases.drushrc.php](https://raw.githubusercontent.com/drush-ops/drush/8.x/examples/example.aliases.drushrc.php) for more information. #!/usr/bin/env sh # # DRUSH WRAPPER # # A wrapper script which launches the Drush that is in your project's /vendor # directory. Copy it to the root of your project and edit as desired. # You may rename this script to 'drush', if doing so does not cause a conflict # (e.g. with a folder __ROOT__/drush). # # Below are options which you might want to add. More info at # `drush topic core-global-options`: # # --local Only discover commandfiles/site aliases/config that are # inside your project dir. # --alias-path A list of directories where Drush will search for site # alias files. # --config A list of paths to config files # --include A list of directories to search for commandfiles. # # Note that it is recommended to use --local when using a drush # wrapper script. # # See the 'drush' script in the Drush installation root (../drush) for # an explanation of the different 'drush' scripts. # # IMPORTANT: Modify the path below if your 'vendor' directory has been # relocated to another location in your composer.json file. # `../vendor/bin/drush.launcher --local $@` is a common variant for # composer-managed Drupal sites. # cd "`dirname $0`" ../vendor/bin/drush.launcher --local "$@" '/path/to/drupal', * 'uri' => 'http://example.com', * ); * @endcode * * With this alias definition, then the following commands * are equivalent: * * $ drush @dev status * $ drush --root=/path/to/drupal --uri=http://example.com status * * See the --uri option documentation below for hints on setting its value. * * Any option that can be placed on the drush commandline * can also appear in an alias definition. * * There are several ways to create alias files. * * + Put each alias in a separate file called ALIASNAME.alias.drushrc.php * + Put multiple aliases in a single file called aliases.drushrc.php * + Put groups of aliases into files called GROUPNAME.aliases.drushrc.php * * Drush will search for aliases in any of these files using * the alias search path. The following locations are examined * for alias files: * * 1. In any path set in $options['alias-path'] in drushrc.php, * or (equivalently) any path passed in via --alias-path=... * on the command line. * 2. In one of the default locations: * a. /etc/drush * b. $HOME/.drush * 3. In one of the site-specific locations: * a. The /drush and /sites/all/drush folders for the current Drupal site * b. The /drush folder in the directory above the current Drupal site * * These locations are searched recursively. If there is a folder called * 'site-aliases' in any search path, then Drush will search for site aliases * only inside that directory. * * The preferred locations for alias files, then, are: * * /etc/drush/site-aliases * $HOME/.drush/site-aliases * $ROOT/drush/site-aliases * $ROOT/sites/all/drush/site-aliases * $ROOT/../drush/site-aliases * * Or any path set in $options['alias-path'] or via --alias-path. * * Folders and files containing other versions of drush in their names will * be *skipped* (e.g. mysite.aliases.drush4rc.php or * drush4/mysite.aliases.drushrc.php). Names containing the current version of * Drush (e.g. mysite.aliases.drush5rc.php) will be loaded. * * Files stored in these locations can be used to create aliases * to local and remote Drupal installations. These aliases can be * used in place of a site specification on the command line, and * may also be used in arguments to certain commands such as * "drush rsync" and "drush sql-sync". * * Alias files that are named after the single alias they contain * may use the syntax for the canonical alias shown at the top of * this file, or they may set values in $options, just * like a drushrc.php configuration file: * * @code * $options['uri'] = 'http://example.com'; * $options['root'] = '/path/to/drupal'; * @endcode * * When alias files use this form, then the name of the alias * is taken from the first part of the alias filename. * * Alias groups (aliases stored together in files called * GROUPNAME.aliases.drushrc.php, as mentioned above) also * create an implicit namespace that is named after the group * name. * * For example: * * @code * # File: mysite.aliases.drushrc.php * $aliases['dev'] = array( * 'root' => '/path/to/drupal', * 'uri' => 'http://example.com', * ); * $aliases['live'] = array( * 'root' => '/other/path/to/drupal', * 'uri' => 'http://example.com', * ); * @endcode * * Then the following special aliases are defined: * - @mysite: An alias named after the groupname may be used to reference all of * the aliases in the group (e.g., `drush @mydrupalsite status`). * - @mysite.dev: A copy of @dev. * - @mysite.live: A copy of @live. * * Thus, aliases defined in an alias group file may be referred to * either by their simple (short) name, or by their full namespace-qualified * name. * * To see an example alias definition for the current bootstrapped * site, use the "site-alias" command with the built-in alias "@self": * * $ drush site-alias @self * * TIP: If you would like to have drush include a 'databases' record * in the output, include the options --with-db and --show-passwords: * * $ drush site-alias @self --with-db --show-passwords * * Drush also supports *remote* site aliases. When a site alias is * defined for a remote site, Drush will use the ssh command to run * the requested command on the remote machine. The simplest remote * alias looks like this: * * @code * $aliases['live'] = array( * 'remote-host' => 'server.domain.com', * 'remote-user' => 'www-admin', * ); * @endcode * * The form above requires that Drush be installed on the remote machine, * and that there also be an alias of the same name defined on that * machine. The remote alias should define the 'root' and 'uri' elements, * as shown in the initial example at the top of this file. * * If you do not wish to maintain site aliases on the remote machine, * then you may define an alias that contains all of the elements * 'remote-host', 'remote-user', 'root' and 'uri'. If you do this, then * Drush will make the remote call using the --root and --uri options * to identify the site, so no site alias is required on the remote server. * * @code * $aliases['live'] = array( * 'remote-host' => 'server.domain.com', * 'remote-user' => 'www-admin', * 'root' => '/other/path/to/drupal', * 'uri' => 'http://example.com', * ); * @endcode * * If you would like to see all of the Drupal sites at a specified * root directory, use the built-in alias "@sites": * * $ drush -r /path/to/drupal site-alias @sites * * It is also possible to define explicit lists of sites using a special * alias list definition. Alias lists contain a list of alias names in * the group, and no other information. For example: * * @code * $aliases['mydevsites'] = array( * 'site-list' => array('@mysite.dev', '@otherside.dev') * ); * @endcode * * The built-in alias "@none" represents the state of no Drupal site; * to ignore the site at the cwd and just see default drush status: * * $ drush @none status * * See `drush help site-alias` for more options for displaying site * aliases. See `drush topic docs-bastion` for instructions on configuring * remote access to a Drupal site behind a firewall via a bastion server. * * Although most aliases will contain only a few options, a number * of settings that are commonly used appear below: * * - 'uri': In Drupal 7 and 8, the value of --uri should always be the same as * when the site is being accessed from a web browser (e.g. http://example.com) * In Drupal 6, the value of --uri should always be the same as the site's folder * name in the 'sites' folder (e.g. default); it is best if the site folder name * matches the URI from the browser, and is consistent on every instance of the * same site (e.g. also use sites/example.com for http://example.com). * - 'root': The Drupal root; must not be specified as a relative path. * - 'remote-host': The fully-qualified domain name of the remote system * hosting the Drupal instance. **Important Note: The remote-host option * must be omitted for local sites, as this option controls various * operations, such as whether or not rsync parameters are for local or * remote machines, and so on. @see hook_drush_sitealias_alter() in drush.api.php * - 'remote-user': The username to log in as when using ssh or rsync. * - 'os': The operating system of the remote server. Valid values * are 'Windows' and 'Linux'. Be sure to set this value for all remote * aliases because the default value is PHP_OS if 'remote-host' * is not set, and 'Linux' (or $options['remote-os']) if it is. Therefore, * if you set a 'remote-host' value, and your remote OS is Windows, if you * do not set the 'OS' value, it will default to 'Linux' and could cause * unintended consequences, particularly when running 'drush sql-sync'. * - 'ssh-options': If the target requires special options, such as a non- * standard port, alternative identity file, or alternative * authentication method, ssh-options can contain a string of extra * options that are used with the ssh command, eg "-p 100" * - 'parent': Deprecated. See "altering aliases", below. * - 'path-aliases': An array of aliases for common rsync targets. * Relative aliases are always taken from the Drupal root. * - '%drush-script': The path to the 'drush' script, or to 'drush.php'. * This is used by backend invoke when drush * runs a drush command. The default is 'drush' on remote machines, or * the full path to drush.php on the local machine. * - '%drush': A read-only property: points to the folder that the drush * script is stored in. * - '%files': Path to 'files' directory. This will be looked up if not * specified. * - '%root': A reference to the Drupal root defined in the 'root' item in the * site alias record. * - 'php': path to custom php interpreter. Windows support limited to Cygwin. * - 'php-options': commandline options for php interpreter, you may * want to set this to '-d error_reporting="E_ALL^E_DEPRECATED"' * - 'variables' : An array of name/value pairs which override Drupal * variables/config. These values take precedence even over settings.php * overrides. * - 'command-specific': These options will only be set if the alias * is used with the specified command. In the example below, the option * `--no-dump` will be selected whenever the @stage alias * is used in any of the following ways: * - `drush @stage sql-sync @self @live` * - `drush sql-sync @stage @live` * - `drush sql-sync @live @stage` * In case of conflicting options, command-specific options in targets * (source and destination) take precedence over command-specific options * in the bootstrapped site, and command-specific options in a destination * alias will take precedence over those in a source alias. * - 'source-command-specific' and 'target-command-specific': Behaves exactly * like the 'command-specific' option, but is applied only if the alias * is used as the source or target, respectively, of an rsync or sql-sync * command. In the example below, `--skip-tables-list=comments` whenever * the alias @live is the target of an sql-sync command, but comments will * be included if @live is the source for the sql-sync command. * - '#peer': Settings that begin with a '#' are not used directly by Drush, and * in fact are removed before making a backend invoke call (for example). * These kinds of values are useful in conjunction with shell aliases. See * `drush topic docs-shell-aliases` for more information on this. * - '#env-vars': An associative array of keys and values that should be set on * the remote side before invoking drush. * - rsync command options have specific requirements in order to * be passed through by Drush. See the comments on the sample below: * * @code * 'command-specific' => array ( * 'core-rsync' => array ( * * // single-letter rsync options are placed in the 'mode' key * // instead of adding '--mode=rultvz' to drush rsync command. * 'mode' => 'rultvz', * * // multi-letter rsync options without values must be set to * // TRUE or NULL to work (i.e. setting $VALUE to 1, 0, or '' * // will not work). * 'delete' => TRUE, * * // if you need multiple excludes, use an rsync exclude file * 'exclude-from' => "'/etc/rsync/exclude.rules'", * * // filter options with white space must be wrapped in "" to preserve * // the inner ''. * 'filter' => "'exclude *.sql'", * * // if you need multple filter options, see rsync merge-file options * 'filter' => "'merge /etc/rsync/default.rules'", * ), * ), * @endcode * * Altering aliases: * * Alias records are written in php, so you may use php code to alter * alias records if you wish. For example: * * @code * $common_live = array( * 'remote-host' => 'myserver.isp.com', * 'remote-user' => 'www-admin', * ); * * $aliases['live'] = array( * 'uri' => 'http://example.com', * 'root' => '/path.to/root', * ) + $common_live; * @endcode * * If you wish, you might want to put $common_live in a separate file, * and include it at the top of each alias file that uses it. * * You may also use a policy file to alter aliases in code as they are * loaded by Drush. See policy_drush_sitealias_alter in * `drush topic docs-policy` for details. * * Some examples appear below. Remove the leading hash signs to enable. */ #$aliases['stage'] = array( # 'uri' => 'http://stage.example.com', # 'root' => '/path/to/remote/drupal/root', # 'remote-host' => 'mystagingserver.myisp.com', # 'remote-user' => 'publisher', # 'os' => 'Linux', # 'path-aliases' => array( # '%drush' => '/path/to/drush', # '%drush-script' => '/path/to/drush/drush', # '%files' => 'sites/mydrupalsite.com/files', # '%custom' => '/my/custom/path', # ), # 'variables' => array( # 'site_name' => 'My Drupal site', # ), # 'command-specific' => array ( # 'sql-sync' => array ( # 'no-dump' => TRUE, # ), # ), # # This shell alias will run `mycommand` when executed via # # `drush @stage site-specific-alias` # 'shell-aliases' => array ( # 'site-specific-alias' => '!mycommand', # ), # ); #$aliases['dev'] = array( # 'uri' => 'http://dev.example.com', # 'root' => '/path/to/drupal/root', # 'variables' => array( # 'mail_system' => array('default-system' => 'DevelMailLog'), # ), # ); #$aliases['server'] = array( # 'remote-host' => 'mystagingserver.myisp.com', # 'remote-user' => 'publisher', # 'os' => 'Linux', # ); #$aliases['live'] = array( # 'uri' => 'http://example.com', # 'root' => $aliases['dev']['root'], # ) + $aliases['server']; # -*- mode: shell-script; mode: flyspell-prog; ispell-local-dictionary: "american" -*- # # Example bash aliases to improve your Drush experience with bash. # Use `drush init` to copy this file to your home directory, rename and # customize it to suit, and source it from your ~/.bashrc file. # # Creates aliases to common Drush commands that work in a global context: # # dr - drush # ddd - drush drupal-directory # dl - drush pm-download # ev - drush php-eval # sa - drush site-alias # sa - drush site-alias --local-only (show local site aliases) # st - drush core-status # use - drush site-set # # Aliases for Drush commands that work on the current drupal site: # # cc - drush cache-clear # cr - drush cache-rebuild # cca - drush cache-clear all # dis - drush pm-disable # en - drush pm-enable # i - drush pm-info # pml - drush pm-list # rf - drush pm-refresh # unin - drush pm-uninstall # up - drush pm-update # upc - drush pm-updatecode # updb - drush updatedb # q - drush sql-query # # Provides several common shell commands to work better with Drush: # # ddd @dev - print the path to the root directory of @dev # cdd @dev - change the current working directory to @dev # lsd @dev - ls root folder of @dev # lsd %files - ls "files" directory of current site # lsd @dev:%devel - ls devel module directory in @dev # @dev st - drush @dev core-status # dssh @live - ssh to the remote server @live points at # gitd @live pull - run `git pull` on the drupal root of @live # # Drush site alias expansion is also done for the cpd command: # # cpd -R @site1:%files @site2:%files # # Note that the 'cpd' alias only works for local sites. Use # `drush rsync` or gitd` to move files between remote sites. # # Aliases are also possible for the following standard # commands. Uncomment their definitions below as desired. # # cd - cddl [*] # ls - lsd # cp - cpd # ssh - dssh # git - gitd # # These standard commands behave exactly the same as they always # do, unless a Drush site specification such as @dev or @live:%files # is used in one of the arguments. # Aliases for common Drush commands that work in a global context. alias dr='drush' alias ddd='drush drupal-directory' alias dl='drush pm-download' alias ev='drush php-eval' alias sa='drush site-alias' alias lsa='drush site-alias --local-only' alias st='drush core-status' alias use='drush site-set' # Aliases for Drush commands that work on the current drupal site alias cc='drush cache-clear' alias cr='drush cache-rebuild' alias cca='drush cache-clear all' alias dis='drush pm-disable' alias en='drush pm-enable' alias pmi='drush pm-info' alias pml='drush pm-list' alias rf='drush pm-refresh' alias unin='drush pm-uninstall' alias up='drush pm-update' alias upc='drush pm-updatecode' alias updb='drush updatedb' alias q='drush sql-query' # Overrides for standard shell commands. Uncomment to enable. Alias # cd='cdd' if you want to be able to use cd @remote to ssh to a # remote site. # alias cd='cddl' # alias ls='lsd' # alias cp='cpd' # alias ssh='dssh' # alias git='gitd' # We extend the cd command to allow convenient # shorthand notations, such as: # cd @site1 # cd %modules # cd %devel # cd @site2:%files # You must use 'cddl' instead of 'cd' if you are not using # the optional 'cd' alias from above. # This is the "local-only" version of the function; # see the cdd function, below, for an expanded implementation # that will ssh to the remote server when a remote site # specification is used. function cddl() { fastcddl "$1" use @self } # Use this function instead of 'cddl' if you have a very large number # of alias files, and the 'cddl' function is getting too slow as a result. # This function does not automatically set your prompt to the site that # you 'cd' to, as 'cddl' does. function fastcddl() { s="$1" if [ -z "$s" ] then builtin cd elif [ "${s:0:1}" == "@" ] || [ "${s:0:1}" == "%" ] then d="$(drush drupal-directory $1 --local-only 2>/dev/null)" if [ $? == 0 ] then echo "cd $d"; builtin cd "$d"; else t="$(drush site-alias $1 >/dev/null 2>/dev/null)" if [ $? == 0 ] then echo "Cannot cd to remote site $s" else echo "Cannot cd to $s" fi fi else builtin cd "$s"; fi } # Works just like the `cddl` shell alias above, with one additional # feature: `cdd @remote-site` works like `ssh @remote-site`, # whereas cd above will fail unless the site alias is local. If # you prefer this behavior, you can add `alias cd='cdd'` to your .bashrc function cdd() { s="$1" if [ -z "$s" ] then builtin cd elif [ "${s:0:1}" == "@" ] || [ "${s:0:1}" == "%" ] then d="$(drush drupal-directory $s 2>/dev/null)" rh="$(drush sa ${s%%:*} --fields=remote-host --format=list)" if [ -z "$rh" ] then echo "cd $d" builtin cd "$d" else if [ -n "$d" ] then c="cd \"$d\" \; bash" drush -s ${s%%:*} ssh --tty drush ${s%%:*} ssh --tty else drush ssh ${s%%:*} fi fi else builtin cd "$s" fi } # Allow `git @site gitcommand` as a shortcut for `cd @site; git gitcommand`. # Also works on remote sites, though. function gitd() { s="$1" if [ -n "$s" ] && [ ${s:0:1} == "@" ] || [ ${s:0:1} == "%" ] then d="$(drush drupal-directory $s 2>/dev/null)" rh="$(drush sa ${s%%:*} --fields=remote-host --format=list)" if [ -n "$rh" ] then drush ${s%%:*} ssh "cd '$d' ; git ${@:2}" else echo cd "$d" \; git "${@:2}" ( cd "$d" "git" "${@:2}" ) fi else "git" "$@" fi } # Get a directory listing on @site or @site:%files, etc, for local or remote sites. function lsd() { p=() r= for a in "$@" ; do if [ ${a:0:1} == "@" ] || [ ${a:0:1} == "%" ] then p[${#p[@]}]="$(drush drupal-directory $a 2>/dev/null)" if [ ${a:0:1} == "@" ] then rh="$(drush sa ${a%:*} --fields=remote-host --format=list)" if [ -n "$rh" ] then r=${a%:*} fi fi elif [ -n "$a" ] then p[${#p[@]}]="$a" fi done if [ -n "$r" ] then drush $r ssh 'ls "${p[@]}"' else "ls" "${p[@]}" fi } # Copy files from or to @site or @site:%files, etc; local sites only. function cpd() { p=() for a in "$@" ; do if [ ${a:0:1} == "@" ] || [ ${a:0:1} == "%" ] then p[${#p[@]}]="$(drush drupal-directory $a --local-only 2>/dev/null)" elif [ -n "$a" ] then p[${#p[@]}]="$a" fi done "cp" "${p[@]}" } # This alias allows `dssh @site` to work like `drush @site ssh`. # Ssh commands, such as `dssh @site ls /tmp`, are also supported. function dssh() { d="$1" if [ ${d:0:1} == "@" ] then drush "$d" ssh "${@:2}" else "ssh" "$@" fi } # Drush checks the current PHP version to ensure compatibility, and fails with # an error if less than the supported minimum (currently 5.4.5). If you would # like to try to run Drush on a lower version of PHP, you can un-comment the # line below to skip this check. Note, however, that this is un-supported. # DRUSH_NO_MIN_PHP=TRUE ; ; Example of a drush php settings override file. ; ; IMPORTANT: This file has no effect when using ; drush.phar. It is only effective when used ; a Composer built Drush is used. When a drush.phar ; hands off execution to a Composer built Drush, ; this file is effective. ; ; IMPORTANT: Before following the instructions in ; this file, first check to see that the cli version ; of php is installed on your system. (e.g. On ; debian systems, `sudo apt-get install php5-cli`.) ; ; Use this file in instances when your system is ; -not- configured to use separate php.ini files for ; webserver and cli use. You can determine which ; php.ini file drush is using by running "drush status". ; If the php.ini file shown is your webserver ini ; file, then rename this file, example.drush.ini, ; to drush.ini and copy it to one of the following ; locations: ; ; 1. Drush installation folder ; 2. User's .drush folder (i.e. ~/.drush/drush.ini) ; 3. System wide configuration folder (i.e. /etc/drush/drush.ini) ; ; If the environment variable DRUSH_INI is defined, ; then the file it specified will be used as drush.ini. ; ; export DRUSH_INI='/path/to/drush.ini' ; ; When in use, the variables defined in this file ; will override the setting values that appear in ; your php.ini file. See the examples below for ; some values that may need to be set in order for ; drush to work. ; ; NOTE: There is a certain amount of overhead ; required for each override, so drush.ini should ; only be used for a relatively small number ; of variables. Comment out any variable that ; has the same value as the webserver php.ini ; to keep the size of the override list small. ; ; To fully specify the value of all php.ini variables, ; copy your webserver php.ini file to one of the ; locations mentioned above (e.g. /etc/drush/php.ini) ; and edit it to suit. Alternately, you may use ; the environment variable PHP_INI to point at ; the file that Drush should use. ; ; export PHP_INI='/path/to/php.ini' ; ; The options listed below are particularly relevant ; to drush. ; ; ; drush needs as much memory as Drupal in order ; to run; make the memory limit setting match ; what you have in your webserver's php.ini. ; memory_limit = 128M ; ; Show all errors and direct them to stderr ; when running drush. ; error_reporting = E_ALL | E_NOTICE | E_STRICT display_errors = stderr ; ; If your php.ini for your webserver is too ; restrictive, you can re-enable functionality ; for drush by adjusting values in this file. ; ; Here are some examples of settings that are ; sometimes set to restrictive values in a ; webserver's php.ini: ; ;safe_mode = ;open_basedir = ;disable_functions = ;disable_classes = TRUE); // Prevent drush ssh command from adding a cd to Drupal root before provided command. # $command_specific['ssh'] = array('cd' => FALSE); // Additional folders to search for scripts. // Separate by : (Unix-based systems) or ; (Windows). # $command_specific['script']['script-path'] = 'sites/all/scripts:profiles/myprofile/scripts'; // Always show release notes when running pm-update or pm-updatecode. # $command_specific['pm-update'] = array('notes' => TRUE); # $command_specific['pm-updatecode'] = array('notes' => TRUE); // Set a predetermined username and password when using site-install. # $command_specific['site-install'] = array('account-name' => 'alice', 'account-pass' => 'secret'); // Use Drupal version specific CLI history instead of per site. # $command_specific['core-cli'] = array('version-history' => TRUE); ; Example makefile ; ---------------- ; This is an example makefile to introduce new users of drush_make to the ; syntax and options available to drush_make. For a full description of all ; options available, see README.txt. ; This make file is a working makefile - try it! Any line starting with a `;` ; is a comment. ; Core version ; ------------ ; Each makefile should begin by declaring the core version of Drupal that all ; projects should be compatible with. core = 7.x ; API version ; ------------ ; Every makefile needs to declare it's Drush Make API version. This version of ; drush make uses API version `2`. api = 2 ; Core project ; ------------ ; In order for your makefile to generate a full Drupal site, you must include ; a core project. This is usually Drupal core, but you can also specify ; alternative core projects like Pressflow. Note that makefiles included with ; install profiles *should not* include a core project. ; Use Pressflow instead of Drupal core: ; projects[pressflow][type] = "core" ; projects[pressflow][download][type] = "file" ; projects[pressflow][download][url] = "http://launchpad.net/pressflow/6.x/6.15.73/+download/pressflow-6.15.73.tar.gz" ; Git clone of Drupal 7.x. Requires the `core` property to be set to 7.x. ; projects[drupal][type] = "core" ; projects[drupal][download][type] = git ; projects[drupal][download][url] = http://git.drupal.org/project/drupal.git projects[] = drupal ; Projects ; -------- ; Each project that you would like to include in the makefile should be ; declared under the `projects` key. The simplest declaration of a project ; looks like this: ; To include the most recent views module: projects[] = views ; This will, by default, retrieve the latest recommended version of the project ; using its update XML feed on Drupal.org. If any of those defaults are not ; desirable for a project, you will want to use the keyed syntax combined with ; some options. ; If you want to retrieve a specific version of a project: ; projects[views] = 2.16 ; Or an alternative, extended syntax: projects[ctools][version] = 1.3 ; Check out the latest version of a project from Git. Note that when using a ; repository as your project source, you must explicitly declare the project ; type so that drush_make knows where to put your project. projects[data][type] = module projects[data][download][type] = git projects[data][download][url] = http://git.drupal.org/project/views.git projects[data][download][revision] = DRUPAL-6--3 ; For projects on drupal.org, some shorthand is available. If any ; download parameters are specified, but not type, the default is git. projects[cck_signup][download][revision] = "2fe932c" ; It is recommended to also specify the corresponding branch so that ; the .info file rewriting can obtain a version string that works with ; the core update module projects[cck_signup][download][branch] = "7.x-1.x" ; Clone a project from github. projects[tao][type] = theme projects[tao][download][type] = git projects[tao][download][url] = git://github.com/developmentseed/tao.git ; If you want to install a module into a sub-directory, you can use the ; `subdir` attribute. projects[admin_menu][subdir] = custom ; To apply a patch to a project, use the `patch` attribute and pass in the URL ; of the patch. projects[admin_menu][patch][687750] = "http://drupal.org/files/issues/admin_menu.long_.31.patch" ; If all projects or libraries share common attributes, the `defaults` ; array can be used to specify these globally, rather than ; per-project. defaults[projects][subdir] = "contrib" # Example makefile # ---------------- # This is an example makefile to introduce new users of drush make to the # syntax and options available to drush make. # This make file is a working makefile - try it! Any line starting with a `#` # is a comment. # Core version # ------------ # Each makefile should begin by declaring the core version of Drupal that all # projects should be compatible with. core: "7.x" # API version # ------------ # Every makefile needs to declare it's Drush Make API version. This version of # drush make uses API version `2`. api: 2 # Core project # ------------ # In order for your makefile to generate a full Drupal site, you must include # a core project. This is usually Drupal core, but you can also specify # alternative core projects like Pressflow. Note that makefiles included with # install profiles *should not* include a core project. # Use Pressflow instead of Drupal core: # projects: # pressflow: # type: "core" # download: # type: "file" # url: "http://launchpad.net/pressflow/6.x/6.15.73/+download/pressflow-6.15.73.tar.gz" # # Git clone of Drupal 7.x. Requires the `core` property to be set to 7.x. # projects # drupal: # type: "core" # download: # url: "http://git.drupal.org/project/drupal.git" projects: drupal: version: ~ # Projects # -------- # Each project that you would like to include in the makefile should be # declared under the `projects` key. The simplest declaration of a project # looks like this: # To include the most recent views module: views: version: ~ # This will, by default, retrieve the latest recommended version of the # project using its update XML feed on Drupal.org. If any of those defaults # are not desirable for a project, you will want to use the keyed syntax # combined with some options. # If you want to retrieve a specific version of a project: # projects: # views: "2.16" # Or an alternative, extended syntax: ctools: version: "1.3" # Check out the latest version of a project from Git. Note that when using a # repository as your project source, you must explicitly declare the project # type so that drush_make knows where to put your project. data: type: "module" download: type: "git" # Note, 'git' is the default, no need to specify. url: "http://git.drupal.org/project/views.git" revision: "7.x-3.x" # For projects on drupal.org, some shorthand is available. If any # download parameters are specified, but not type, the default is git. cck_signup: download: revision: "2fe932c" # It is recommended to also specify the corresponding branch so that # the .info file rewriting can obtain a version string that works with # the core update module branch: "7.x-1.x" # Clone a project from github. tao: type: theme download: url: "git://github.com/developmentseed/tao.git" # If you want to install a module into a sub-directory, you can use the # `subdir` attribute. admin_menu: subdir: custom # To apply patches to a project, use the `patch` attribute and pass in the URL # of the patch, one per line prefaced with `- `. patch: - "http://drupal.org/files/issues/admin_menu.long_.31.patch" # If all projects or libraries share common attributes, the `defaults` # array can be used to specify these globally, rather than # per-project. defaults: projects: subdir: "contrib" # -*- mode: shell-script; mode: flyspell-prog; ispell-local-dictionary: "american" -*- # # Example PS1 prompt. # # Use `drush init` to copy this to ~/.drush/drush.prompt.sh, and source it in ~/.bashrc # # Features: # # Displays Git repository and Drush alias status in your prompt. if [ -n "$(type -t __git_ps1)" ] && [ "$(type -t __git_ps1)" = function ] && [ "$(type -t __drush_ps1)" ] && [ "$(type -t __drush_ps1)" = function ]; then # This line enables color hints in your Drush prompt. Modify the below # __drush_ps1_colorize_alias() to customize your color theme. DRUSH_PS1_SHOWCOLORHINTS=true # Git offers various prompt customization options as well as seen in # https://github.com/git/git/blob/master/contrib/completion/git-prompt.sh. # Adjust the following lines to enable the corresponding features: # GIT_PS1_SHOWDIRTYSTATE=true GIT_PS1_SHOWUPSTREAM=auto # GIT_PS1_SHOWSTASHSTATE=true # GIT_PS1_SHOWUNTRACKEDFILES=true GIT_PS1_SHOWCOLORHINTS=true # The following line sets your bash prompt according to this example: # # username@hostname ~/working-directory (git-branch)[@drush-alias] $ # # See http://ss64.com/bash/syntax-prompt.html for customization options. export PROMPT_COMMAND='__git_ps1 "\u@\h \w" "$(__drush_ps1 "[%s]") \\\$ "' # PROMPT_COMMAND is used in the example above rather than PS1 because neither # Git nor Drush color hints are compatible with PS1. If you don't want color # hints, however, and prefer to use PS1, you can still do so by commenting out # the PROMPT_COMMAND line above and uncommenting the PS1 line below: # # export PS1='\u@\h \w$(__git_ps1 " (%s)")$(__drush_ps1 "[%s]")\$ ' __drush_ps1_colorize_alias() { if [[ -n ${ZSH_VERSION-} ]]; then local COLOR_BLUE='%F{blue}' local COLOR_CYAN='%F{cyan}' local COLOR_GREEN='%F{green}' local COLOR_MAGENTA='%F{magenta}' local COLOR_RED='%F{red}' local COLOR_WHITE='%F{white}' local COLOR_YELLOW='%F{yellow}' local COLOR_NONE='%f' else # Using \[ and \] around colors is necessary to prevent issues with # command line editing/browsing/completion. local COLOR_BLUE='\[\e[94m\]' local COLOR_CYAN='\[\e[36m\]' local COLOR_GREEN='\[\e[32m\]' local COLOR_MAGENTA='\[\e[35m\]' local COLOR_RED='\[\e[91m\]' local COLOR_WHITE='\[\e[37m\]' local COLOR_YELLOW='\[\e[93m\]' local COLOR_NONE='\[\e[0m\]' fi # Customize your color theme below. case "$__DRUPAL_SITE" in *.live|*.prod) local ENV_COLOR="$COLOR_RED" ;; *.stage|*.test) local ENV_COLOR="$COLOR_YELLOW" ;; *.local) local ENV_COLOR="$COLOR_GREEN" ;; *) local ENV_COLOR="$COLOR_BLUE" ;; esac __DRUPAL_SITE="${ENV_COLOR}${__DRUPAL_SITE}${COLOR_NONE}" } fi #!/usr/bin/env sh # # Git bisect is a helpful way to discover which commit an error # occurred in. This example file gives simple instructions for # using git bisect with Drush to quickly find erroneous commits # in Drush commands or Drupal modules, presuming that you can # trigger the error condition via Drush (e.g. using `drush php-eval`). # # Follow these simple steps: # # $ git bisect start # $ git bisect bad # Tell git that the current commit does not work # $ git bisect good bcadd5a # Tell drush that the commithash 12345 worked fine # $ git bisect run mytestscript.sh # # 'git bisect run' will continue to call 'git bisect good' and 'git bisect bad', # based on whether the script's exit code was 0 or 1, respectively. # # Replace 'mytestscript.sh' in the example above with a custom script that you # write yourself. Use the example script at the end of this document as a # guide. Replace the example command with one that calls the Drush command # that you would like to test, and replace the 'grep' string with a value # that appears when the error exists in the commit, but does not appear when # commit is okay. # # If you are using Drush to test Drupal or an external Drush module, use: # # $ git bisect run drush mycommand --strict=2 # # This presumes that there is one or more '[warning]' or '[error]' # messages emitted when there is a problem, and no warnings or errors # when the commit is okay. Omit '--strict=2' to ignore warnings, and # signal failure only when 'error' messages are emitted. # # If you need to test for an error condition explicitly, to find errors # that do not return any warning or error log messages on their own, you # can use the Drush php-eval command to force an error when `myfunction()` # returns FALSE. Replace 'myfunction()' with the name of an appropriate # function in your module that can be used to detect the error condition # you are looking for. # # $ git bisect run drush ev 'if(!myfunction()) { return drush_set_error("ERR"); }' # drush mycommand --myoption 2>&1 | grep -q 'string that indicates there was a problem' if [ $? == 0 ] ; then exit 1 else exit 0 fi #!/usr/bin/env drush // // This example demonstrates how to write a drush // "shebang" script. These scripts start with the // line "#!/usr/bin/env drush" or "#!/full/path/to/drush". // // See `drush topic docs-scripts` for more information. // drush_print("Hello world!"); drush_print(); drush_print("The arguments to this command were:"); // // If called with --everything, use drush_get_arguments // to print the commandline arguments. Note that this // call will include 'php-script' (the drush command) // and the path to this script. // if (drush_get_option('everything')) { drush_print(" " . implode("\n ", drush_get_arguments())); } // // If --everything is not included, then use // drush_shift to pull off the arguments one at // a time. drush_shift only returns the user // commandline arguments, and does not include // the drush command or the path to this script. // else { while ($arg = drush_shift()) { drush_print(' ' . $arg); } } drush_print(); // // We can check which site was bootstrapped via // the '@self' alias, which is defined only if // there is a bootstrapped site. // $self_record = drush_sitealias_get_record('@self'); if (empty($self_record)) { drush_print('No bootstrapped site.'); } else { drush_print('The following site is bootstrapped:'); _drush_sitealias_print_record($self_record); } "Makes a delicious sandwich.", 'arguments' => array( 'filling' => 'The type of the sandwich (turkey, cheese, etc.). Defaults to ascii.', ), 'options' => array( 'spreads' => array( 'description' => 'Comma delimited list of spreads.', 'example-value' => 'mayonnaise,mustard', ), ), 'examples' => array( 'drush mmas turkey --spreads=ketchup,mustard' => 'Make a terrible-tasting sandwich that is lacking in pickles.', ), 'aliases' => array('mmas'), // No bootstrap at all. 'bootstrap' => DRUSH_BOOTSTRAP_NONE, ); // The 'sandwiches-served' command. Informs how many 'mmas' commands // completed. $items['sandwiches-served'] = array( 'description' => "Report how many sandwiches we have made.", 'examples' => array( 'drush sandwiches-served' => 'Show how many sandwiches we have served.', ), 'aliases' => array('sws'), // Example output engine data: command returns a single keyed // data item (e.g. array("served" => 1)) that can either be // printed with a label (e.g. "served: 1"), or output raw with // --pipe (e.g. "1"). 'engines' => array( 'outputformat' => array( 'default' => 'key-value', 'pipe-format' => 'string', 'label' => 'Sandwiches Served', 'require-engine-capability' => array('format-single'), ), ), // No bootstrap at all. 'bootstrap' => DRUSH_BOOTSTRAP_NONE, ); // The 'spreads-status' command. Prints a table about available spreads. $items['spreads-status'] = array( 'description' => "Show a table of information about available spreads.", 'examples' => array( 'drush spreads-status' => 'Show a table of spreads.', ), 'aliases' => array('sps'), // Example output engine data: command returns a deep array // that can either be printed in table format or as a json array. 'engines' => array( 'outputformat' => array( 'default' => 'table', 'pipe-format' => 'json', // Commands that return deep arrays will usually use // machine-ids for the column data. A 'field-labels' // item maps from the machine-id to a human-readable label. 'field-labels' => array( 'name' => 'Name', 'description' => 'Description', 'available' => 'Num', 'taste' => 'Taste', ), // In table format, the 'column-widths' item is consulted // to determine the default weights for any named column. 'column-widths' => array( 'name' => 10, 'available' => 3, ), 'require-engine-capability' => array('format-table'), ), ), // No bootstrap at all. 'bootstrap' => DRUSH_BOOTSTRAP_NONE, ); // Commandfiles may also add topics. These will appear in // the list of topics when `drush topic` is executed. // To view this topic, run `drush --include=/full/path/to/examples topic` $items['sandwich-exposition'] = array( 'description' => 'Ruminations on the true meaning and philosophy of sandwiches.', 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_NONE, 'callback' => 'drush_print_file', 'callback arguments' => array(dirname(__FILE__) . '/sandwich-topic.txt'), ); return $items; } /** * Implements hook_drush_help(). * * This function is called whenever a drush user calls * 'drush help '. This hook is optional. If a command * does not implement this hook, the command's description is used instead. * * This hook is also used to look up help metadata, such as help * category title and summary. See the comments below for a description. */ function sandwich_drush_help($section) { switch ($section) { case 'drush:make-me-a-sandwich': return dt("This command will make you a delicious sandwich, just how you like it."); // The 'title' meta item is used to name a group of // commands in `drush help`. If a title is not defined, // the default is "All commands in ___", with the // specific name of the commandfile (e.g. sandwich). // Command files with less than four commands will // be placed in the "Other commands" section, _unless_ // they define a title. It is therefore preferable // to not define a title unless the file defines a lot // of commands. case 'meta:sandwich:title': return dt("Sandwich commands"); // The 'summary' meta item is displayed in `drush help --filter`, // and is used to give a general idea what the commands in this // command file do, and what they have in common. case 'meta:sandwich:summary': return dt("Automates your sandwich-making business workflows."); } } /** * Implements drush_hook_COMMAND_validate(). * * The validate command should exit with * `return drush_set_error(...)` to stop execution of * the command. In practice, calling drush_set_error * OR returning FALSE is sufficient. See drush.api.php * for more details. */ function drush_sandwich_make_me_a_sandwich_validate() { if (drush_is_windows()) { // $name = drush_get_username(); // @todo Implement check for elevated process using w32api // as sudo is not available for Windows // @see http://php.net/manual/en/book.w32api.php // @see http://social.msdn.microsoft.com/Forums/en/clr/thread/0957c58c-b30b-4972-a319-015df11b427d } else { $name = posix_getpwuid(posix_geteuid()); if ($name['name'] !== 'root') { return drush_set_error('MAKE_IT_YOUSELF', dt('What? Make your own sandwich.')); } } } /** * Implements drush_hook_COMMAND(). * * The command callback is where the action takes place. * * The function name should be same as command name but with dashes turned to * underscores and 'drush_commandfile_' prepended, where 'commandfile' is * taken from the file 'commandfile.drush.inc', which in this case is * 'sandwich'. Note also that a simplification step is also done in instances * where the commandfile name is the same as the beginning of the command name, * "drush_example_example_foo" is simplified to just "drush_example_foo". * To also implement a hook that is called before your command, implement * "drush_hook_pre_example_foo". For a list of all available hooks for a * given command, run drush in --debug mode. * * If for some reason you do not want your hook function to be named * after your command, you may define a 'callback' item in your command * object that specifies the exact name of the function that should be * called. * * In this function, all of Drupal's API is (usually) available, including * any functions you have added in your own modules/themes. * * @see drush_invoke() * @see drush.api.php */ function drush_sandwich_make_me_a_sandwich($filling = 'ascii') { $str_spreads = ''; // Read options with drush_get_option. Note that the options _must_ // be documented in the $items structure for this command in the 'command' // hook. See `drush topic docs-commands` for more information. if ($spreads = drush_get_option('spreads')) { $list = implode(' and ', explode(',', $spreads)); $str_spreads = ' with just a dash of ' . $list; } $msg = dt('Okay. Enjoy this !filling sandwich!str_spreads.', array('!filling' => $filling, '!str_spreads' => $str_spreads) ); drush_print("\n" . $msg . "\n"); if (drush_get_context('DRUSH_NOCOLOR')) { $filename = dirname(__FILE__) . '/sandwich-nocolor.txt'; } else { $filename = dirname(__FILE__) . '/sandwich.txt'; } drush_print(file_get_contents($filename)); // Find out how many sandwiches have been served, and set // the cached value to one greater. $served = drush_sandwich_sandwiches_served(); drush_cache_set(drush_get_cid('sandwiches-served'), $served + 1); } /** * Implements drush_hook_COMMAND(). * * Demonstrates how to return a simple value that is transformed by * the selected formatter to display either with a label (using the * key-value formatter) or as the raw value itself (using the string formatter). */ function drush_sandwich_sandwiches_served() { $served = 0; $served_object = drush_cache_get(drush_get_cid('sandwiches-served')); if ($served_object) { $served = $served_object->data; } // In the default format, key-value, this return value // will print " Sandwiches Served : 1". In the default pipe // format, only the array value ("1") is returned. return $served; } /** * Implements drush_hook_COMMAND(). * * This ficticious command shows how a deep array can be constructed * and used as a command return value that can be output by different * output formatters. */ function drush_sandwich_spreads_status() { return array( 'ketchup' => array( 'name' => 'Ketchup', 'description' => 'Some say its a vegetable, but we know its a sweet spread.', 'available' => '7', 'taste' => 'sweet', ), 'mayonnaise' => array( 'name' => 'Mayonnaise', 'description' => 'A nice dairy-free spead.', 'available' => '12', 'taste' => 'creamy', ), 'mustard' => array( 'name' => 'Mustard', 'description' => 'Pardon me, but could you please pass that plastic yellow bottle?', 'available' => '8', 'taste' => 'tangy', ), 'pickles' => array( 'name' => 'Pickles', 'description' => 'A necessary part of any sandwich that does not taste terrible.', 'available' => '63', 'taste' => 'tasty', ), ); } /** * Command argument complete callback. * * Provides argument values for shell completion. * * @return array * Array of popular fillings. */ function sandwich_make_me_a_sandwich_complete() { return array('values' => array('turkey', 'cheese', 'jelly', 'butter')); }  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  . . . . . . . . . . .:8 ;t;;t;;;;:..:;%SX888@X%t;.. . . .   . . .. . . . . . .%t%;%@%%%%%%%%%%X@8888XS%t;...:;ttt%X. .  . . . . . . . . .X:8%X%%%XS%%%%%%%XS%%%%%@%%%%X%%%@%S88 .   . . . . . . . . X@ @%%%X8X%%%88%%%8X%%%%%%%%%XXt@8@88@. .  . . . . . . . .t@tS;%%8XSX%@XSX%@XSS%8@@88X@888X8SS S;S.   . . . . . . .@%XS%%%%%S8@X%@8%XXSSXX%S@SSSX888.;@ 888@ . .   . . . . . :.8:S%%%XS8X@@X%S@SSSS8SXSXXX%X88X:;@8@:S  88S. .  . . . .8%S8%%%%%%8@SSSXXXSXSXSXSXSSS8S888 :@%:%XX:%8%:X:   . . .:8 %%%%@%%8@S%%XXSXSSSS8S@X%XSXX88 ;@X;SX88X8;%X88t.   . . 88S%S%%%%8XSSXSX@@S@%XS8@SS%@S%888 88@S:8. .;.@%X:@8;.   . .  88.8888888@XX 888888%X%@XX 88SS8@@;S@8.%;8@S%%:8  .  . .  S%:8 @SSSS8 @@8@8 8 88888888@%S:8:S8 @..%S SXX8888;. .  . . %:8S8888@88SXS S S::X@.8.8 X%S%8X:X88..% @@.S.%% .;. .   . .XX8@8;;%%t;;;;:@X@888888@888888.88S;8:8  ... . . .  . . 8.;;@8@8:%%%%%t.8@%ttX@8@@@S8%8 X8S;X:@; :... . . . . .  . tS:8@;88.;:8888X8S:.tX88888X88  S8tStS88 :.. . . . . . .   .:X;;:t%;tt%888S@8XS888@8.:tt@;88.tXXX8:::... . . . . . . .  .:X8St:8SXS XS8@X 8.8%888%X8@@X88tXS8t; . . . . . . . . .   . :8888.88888888X@@X @ X X%S%;88;8t .. . . . . . . . .  ... ..: . .@@888%St @ @ 8SS 8:; . . . . . . . . . . .  . . . ..::. ..:;;::::. ... . . . . . . . . . . . .  .. . . . . .. . . . . .. . . . . . . . . . . . . .  '/srv/www/drupal', * 'uri' => 'site.com', * 'target-command-specific' => array( * 'sql-sync' => array( * 'enable' => array('devel', 'hacked'), * 'disable' => array('securepages'), * 'permission' => array( * 'authenticated user' => array( * 'add' => array('access devel information', 'access environment indicator'), * 'remove' => 'change own password', * ), * 'anonymous user' => array( * 'add' => 'access environment indicator', * ), * ), * ), * ), * ); * @endcode * * To use this feature, copy the 'target-command-specific' * item from the example alias above, place it in your development * site aliases, and customize the development module list * to suit. You must also copy the sync_enable.drush.inc * file to a location where Drush will find it, such as * $HOME/.drush. See `drush topic docs-commands` for more * information. * * To set variables on a development site: * * Instead of calling variable_set and variable_delete in a post-sync * hook, consider adding $conf variables to settings.php. * * For example: * * $conf['error_level'] = 2; * error_reporting(E_ALL); * ini_set('display_errors', TRUE); * ini_set('display_startup_errors', TRUE); * $conf['preprocess_css'] = 0; * $conf['cache'] = 0; * $conf['googleanalytics_account'] = ''; */ /** * Implements hook_drush_help_alter(). * * When a hook extends a command with additional options, it must * implement help alter and declare the option(s). Doing so will add * the option to the help text for the modified command, and will also * allow the new option to be specified on the command line. Without * this, Drush will fail with an error when a user attempts to use * the option. */ function sync_enable_drush_help_alter(&$command) { if ($command['command'] == 'sql-sync') { $command['options']['updb'] = "Apply database updates on the target database after the sync operation has completed."; $command['options']['enable'] = "Enable the specified modules in the target database after the sync operation has completed."; $command['options']['disable'] = "Disable the specified modules in the target database after the sync operation has completed."; $command['options']['permission'] = "Add or remove permissions from a role in the target database after the sync operation has completed. The value of this option must be an array, so it may only be specified in a site alias record or drush configuration file. See `drush topic docs-example-sync-extension`."; } } /** * Implements drush_hook_post_COMMAND(). * * The post hook is only called if the sql-sync operation completes * without an error. When called, we check to see if the user specified * any modules to enable/disable. If so, we will call pm-enable/pm-disable on * each module. */ function drush_sync_enable_post_sql_sync($source = NULL, $destination = NULL) { $updb = drush_get_option('updb', FALSE); if ($updb) { drush_log('Run database updates', 'ok'); drush_invoke_process($destination, 'updatedb', array(), array('yes' => TRUE)); } $modules_to_enable = drush_get_option_list('enable'); if (!empty($modules_to_enable)) { drush_log(dt("Enable !modules post-sql-sync", array('!modules' => implode(',', $modules_to_enable))), 'ok'); drush_invoke_process($destination, 'pm-enable', $modules_to_enable, array('yes' => TRUE)); } $modules_to_disable = drush_get_option_list('disable'); if (!empty($modules_to_disable)) { drush_log(dt("Disable !modules post-sql-sync", array('!modules' => implode(',', $modules_to_disable))), 'ok'); drush_invoke_process($destination, 'pm-disable', $modules_to_disable, array('yes' => TRUE)); } $permissions_table = drush_get_option('permission'); if (!empty($permissions_table)) { foreach ($permissions_table as $role_name => $actions) { if (array_key_exists('add', $actions)) { $permissions_to_add = is_array($actions['add']) ? $actions['add'] : explode(', ', $actions['add']); foreach ($permissions_to_add as $permission) { $values = drush_invoke_process($destination, 'role-add-perm', array($role_name, $permission), array(), array('integrate' => TRUE)); } } if (array_key_exists('remove', $actions)) { $permissions_to_remove = is_array($actions['remove']) ? $actions['remove'] : explode(', ', $actions['remove']); foreach ($permissions_to_remove as $permission) { $values = drush_invoke_process($destination, 'role-remove-perm', array($role_name, $permission), array(), array('integrate' => TRUE)); } } } } } '/srv/www/drupal', * 'uri' => 'staging.site.com', * 'source-command-specific' => array( * 'sql-sync' => array( * 'http-sync' => 'https://staging.site.com/protected-directory/site-database-dump.sql', * 'http-sync-user' => 'wwwadmin', * 'http-sync-password' => 'secretsecret', * ), * ), * ); * @endcode * * To use this feature, copy the 'source-command-specific' * item from the example alias above, place it in your staging * site aliases, and custom the access credentials as * necessary. You must also copy the sync_via_http.drush.inc * file to a location where Drush will find it, such as * $HOME/.drush. See `drush topic docs-commands` for more * information. * * IMPORTANT NOTE: This example does not cause the sql dump * to be performed; it is presumed that the dump file already * exists at the provided URL. For a full solution, a web page * that initiated an sql-dump (or perhaps a local sql-sync followed * by an sql-sanitize and then an sql-dump) would be necessary. */ /** * Implements hook_drush_help_alter(). * * When a hook extends a command with additional options, it must * implement help alter and declare the option(s). Doing so will add * the option to the help text for the modified command, and will also * allow the new option to be specified on the command line. Without * this, Drush will fail with an error when a user attempts to use * the option. */ function sync_via_http_drush_help_alter(&$command) { if ($command['command'] == 'sql-sync') { $command['options']['http-sync'] = "Copy the database via http instead of rsync. Value is the url that the existing database dump can be found at."; $command['sub-options']['http-sync']['http-sync-user'] = "Username for the protected directory containing the sql dump."; $command['sub-options']['http-sync']['http-sync-password'] = "Password for the same directory."; } } /** * Implements drush_hook_pre_COMMAND(). * * During the pre hook, determine if the http-sync option has been * specified. If it has been, then disable the normal ssh + rsync * dump-and-transfer that sql-sync usually does, and transfer the * database dump via an http download. */ function drush_sync_via_http_pre_sql_sync($source = NULL, $destination = NULL) { $sql_dump_download_url = drush_get_option('http-sync'); if (!empty($sql_dump_download_url)) { $user = drush_get_option('http-sync-user', FALSE); $password = drush_get_option('http-sync-password', FALSE); $source_dump_file = _drush_sync_via_http_download_file($sql_dump_download_url, $user, $password); if ($source_dump_file === FALSE) { return drush_set_error('DRUSH_CANNOT_DOWNLOAD', dt("The URL !url could not be downloaded.", array('!url' => $sql_dump_download_url))); } drush_set_option('target-dump', $source_dump_file); drush_set_option('no-dump', TRUE); drush_set_option('no-sync', TRUE); } } /** * Downloads a files. * * Optionaly uses user authentication, using either wget or curl, as available. */ function _drush_sync_via_http_download_file($url, $user = FALSE, $password = FALSE, $destination = FALSE, $overwrite = TRUE) { static $use_wget; if ($use_wget === NULL) { $use_wget = drush_shell_exec('which wget'); } $destination_tmp = drush_tempnam('download_file'); if ($use_wget) { if ($user && $password) { drush_shell_exec("wget -q --timeout=30 --user=%s --password=%s -O %s %s", $user, $password, $destination_tmp, $url); } else { drush_shell_exec("wget -q --timeout=30 -O %s %s", $destination_tmp, $url); } } else { if ($user && $password) { drush_shell_exec("curl -s -L --connect-timeout 30 --user %s:%s -o %s %s", $user, $password, $destination_tmp, $url); } else { drush_shell_exec("curl -s -L --connect-timeout 30 -o %s %s", $destination_tmp, $url); } } if (!drush_get_context('DRUSH_SIMULATE')) { if (!drush_file_not_empty($destination_tmp) && $file = @file_get_contents($url)) { @file_put_contents($destination_tmp, $file); } if (!drush_file_not_empty($destination_tmp)) { // Download failed. return FALSE; } } if ($destination) { drush_move_dir($destination_tmp, $destination, $overwrite); return $destination; } return $destination_tmp; } "Retrieve and display xkcd cartoons.", 'arguments' => array( 'search' => 'Optional argument to retrive the cartoons matching an index number, keyword search or "random". If omitted the latest cartoon will be retrieved.', ), 'options' => array( 'image-viewer' => 'Command to use to view images (e.g. xv, firefox). Defaults to "display" (from ImageMagick).', 'google-custom-search-api-key' => 'Google Custom Search API Key, available from https://code.google.com/apis/console/. Default key limited to 100 queries/day globally.', ), 'examples' => array( 'drush xkcd' => 'Retrieve and display the latest cartoon.', 'drush xkcd sandwich' => 'Retrieve and display cartoons about sandwiches.', 'drush xkcd 123 --image-viewer=eog' => 'Retrieve and display cartoon #123 in eog.', 'drush xkcd random --image-viewer=firefox' => 'Retrieve and display a random cartoon in Firefox.', ), 'aliases' => array('xkcd'), // No bootstrap at all. 'bootstrap' => DRUSH_BOOTSTRAP_NONE, ); return $items; } /** * Implements hook_drush_help(). * * This function is called whenever a drush user calls * 'drush help '. This hook is optional. If a command * does not implement this hook, the command's description is used instead. * * This hook is also used to look up help metadata, such as help * category title and summary. See the comments below for a description. */ function xkcd_drush_help($section) { switch ($section) { case 'drush:xkcd-fetch': return dt("A command line tool (1) for a web site tool (2), that emulates (badly) a web based tool (3) that emulates (badly) a command line tool (4) to access a web site (5) with awesome geek humor.\n (1) Drush (2) Drupal (3) http://uni.xkcd.com/ (4) BASH (5) http://xkcd.com/"); } } /** * Implements drush_hook_COMMAND(). * * The command callback is where the action takes place. * * The function name should be same as command name but with dashes turned to * underscores and 'drush_commandfile_' prepended, where 'commandfile' is * taken from the file 'commandfile.drush.inc', which in this case is * 'sandwich'. Note also that a simplification step is also done in instances * where the commandfile name is the same as the beginning of the command name, * "drush_example_example_foo" is simplified to just "drush_example_foo". * To also implement a hook that is called before your command, implement * "drush_hook_pre_example_foo". For a list of all available hooks for a * given command, run drush in --debug mode. * * If for some reason you do not want your hook function to be named * after your command, you may define a 'callback' item in your command * object that specifies the exact name of the function that should be * called. * * In this function, all of Drupal's API is (usually) available, including * any functions you have added in your own modules/themes. * * @see drush_invoke() * @see drush.api.php * * @param string $search * An optional string with search keyworks, cartoon ID or "random". */ function drush_xkcd_fetch($search = '') { if (empty($search)) { drush_xkcd_display('http://xkcd.com'); } elseif (is_numeric($search)) { drush_xkcd_display('http://xkcd.com/' . $search); } elseif ($search == 'random') { $xkcd_response = @json_decode(file_get_contents('http://xkcd.com/info.0.json')); if (!empty($xkcd_response->num)) { drush_xkcd_display('http://xkcd.com/' . rand(1, $xkcd_response->num)); } } else { // This uses an API key with a limited number of searches per. $search_response = @json_decode(file_get_contents('https://www.googleapis.com/customsearch/v1?key=' . drush_get_option('google-custom-search-api-key', 'AIzaSyDpE01VDNNT73s6CEeJRdSg5jukoG244ek') . '&cx=012652707207066138651:zudjtuwe28q&q=' . $search)); if (!empty($search_response->items)) { foreach ($search_response->items as $item) { drush_xkcd_display($item->link); } } else { drush_set_error('DRUSH_XKCD_SEARCH_FAIL', dt('The search failed or produced no results.')); } } } /** * Display a given XKCD cartoon. * * Retrieve and display a table of metadata for an XKCD cartoon, then retrieve * and display the cartoon using a specified image viewer. * * @param string $url * A string with the URL of the cartoon to display. */ function drush_xkcd_display($url) { $xkcd_response = @json_decode(file_get_contents($url . '/info.0.json')); if (!empty($xkcd_response->num)) { $data = (array) $xkcd_response; $data['date'] = $data['year'] . '/' . $data['month'] . '/' . $data['day']; unset($data['safe_title'], $data['news'], $data['link'], $data['year'], $data['month'], $data['day']); drush_print_table(drush_key_value_to_array_table($data)); $img = drush_download_file($data['img']); drush_register_file_for_deletion($img); drush_shell_exec(drush_get_option('image-viewer', 'display') . ' ' . $img); } else { drush_set_error('DRUSH_XKCD_METADATA_FAIL', dt('Unable to retrieve cartoon metadata.')); } } setIncludeFilesAtBase(false) ->setSearchLocations(['Commands']) ->setSearchPattern('#.*Commands.php$#'); } return $discovery; } /** * Initialize and cache the command factory. Drush 9 uses dependency injection. * * @return AnnotatedCommandFactory */ function annotationcommand_adapter_get_factory() { static $factory; if (!isset($factory)) { $factory = new AnnotatedCommandFactory(); $factory->commandProcessor()->hookManager()->add('annotatedcomand_adapter_backend_result', HookManager::EXTRACT_OUTPUT); $formatter = new FormatterManager(); $formatter->addDefaultFormatters(); $formatter->addDefaultSimplifiers(); $factory->commandProcessor()->setFormatterManager($formatter); } return $factory; } /** * Fetch the command processor from the factory. * * @return AnnotatedCommandFactory */ function annotationcommand_adapter_get_processor() { $factory = annotationcommand_adapter_get_factory(); return $factory->commandProcessor(); } /** * Fetch the formatter manager from the command processor * * @return FormatterManager */ function annotatedcomand_adapter_get_formatter() { $commandProcessor = annotationcommand_adapter_get_processor(); return $commandProcessor->formatterManager(); } /** * Callback function called by HookManager::EXTRACT_OUTPUT to set * the backend result. */ function annotatedcomand_adapter_backend_result($structured_data) { $return = drush_backend_get_result(); if (empty($return)) { drush_backend_set_result($structured_data); } } /** * Return the cached commands built by annotationcommand_adapter_discover. * @see drush_get_commands() */ function annotationcommand_adapter_commands() { $annotation_commandfiles = drush_get_context('DRUSH_ANNOTATED_COMMANDFILES'); // Remove any entry in the commandfiles list from an ignored module. $ignored = implode('|', drush_get_option_list('ignored-modules')); $regex = "#/(modules|themes|profiles)(/|/.*/)($ignored)/#"; foreach ($annotation_commandfiles as $key => $path) { if (preg_match($regex, $path)) { unset($annotation_commandfiles[$key]); } } $commands = annotationcommand_adapter_get_commands($annotation_commandfiles); $module_service_commands = drush_get_context('DRUSH_MODULE_SERVICE_COMMANDS'); return array_merge($commands, $module_service_commands); } /** * Search for annotation commands at the provided search path. * @see _drush_find_commandfiles() */ function annotationcommand_adapter_discover($searchpath, $phase = false, $phase_max = false) { if (empty($searchpath)) { return; } if (($phase >= DRUSH_BOOTSTRAP_DRUPAL_SITE) && (drush_drupal_major_version() >= 8)) { return; } $annotation_commandfiles = []; // Assemble a cid specific to the bootstrap phase and searchpaths. // Bump $cf_version when making a change to a dev version of Drush // that invalidates the commandfile cache. $cf_version = 1; $cid = drush_get_cid('annotationfiles-' . $phase, array(), array_merge($searchpath, array($cf_version))); $command_cache = drush_cache_get($cid); if (isset($command_cache->data)) { $annotation_commandfiles = $command_cache->data; } else { // Check to see if this is the Drush searchpath for instances where we are // NOT going to do a full bootstrap (e.g. when running a help command) if (($phase == DRUSH_BOOTSTRAP_DRUPAL_SITE) && ($phase_max < DRUSH_BOOTSTRAP_DRUPAL_FULL)) { $searchpath = annotationcommand_adapter_refine_searchpaths($searchpath); } $discovery = annotationcommand_adapter_get_discovery(); $annotation_commandfiles = $discovery->discoverNamespaced($searchpath, '\Drupal'); drush_cache_set($cid, $annotation_commandfiles); } drush_set_context( 'DRUSH_ANNOTATED_COMMANDFILES', array_merge( drush_get_context('DRUSH_ANNOTATED_COMMANDFILES'), $annotation_commandfiles ) ); } /** * This function is set as the $command['callback'] for Symfony Console commands * e.g. those provided by Drupal 8 modules. Note that Drush 9 calls these with * Symfony's Application::run() method, but Drush 8 continues to use the * legacy Drush command dispatcher for all commands. * * @return bolean false if command failed (expect drush_set_error was called in this case) */ function annotationcommand_adapter_run_console_command() { $args = func_get_args(); $command = drush_get_command(); $console_command = $command['drush-console-command']; // TODO: Build an appropriate input object $input = annotationcommand_adapter_build_input($console_command, $args); $output = new ConsoleOutput(); $result = $console_command->run($input, $output); return $result; } /** * TODO: This could probably just be a DrushInputAdapter now. */ function annotationcommand_adapter_build_input($console_command, $userArgs) { $args = []; $defaultOptions = []; $definition = $console_command->getDefinition(); $inputArguments = $definition->getArguments(); foreach ($inputArguments as $key => $inputOption) { $value = array_shift($userArgs); if (!isset($value)) { $value = $inputOption->getDefault(); } $args[$key] = $value; } $inputOptions = $definition->getOptions(); foreach ($inputOptions as $option => $inputOption) { $defaultOptions[$option] = $inputOption->getDefault(); } foreach ($defaultOptions as $option => $value) { $args["--$option"] = drush_get_option($option, $value); } // TODO: Need to add global options. Note that ArrayInput is validated. $input = new ArrayInput($args, $definition); return $input; } /** * Collect all of the options defined in every relevant context, and * merge them together to form the options array. * * @return array */ function annotationcommand_adapter_get_options($command) { $default_options = isset($command['consolidation-option-defaults']) ? $command['consolidation-option-defaults'] : []; $options = drush_redispatch_get_options() + $default_options; $options += drush_get_merged_options(); return $options; } /** * This function is set as the $command['callback'] for commands that have * been converted to annotated commands. Note that Drush 9 calls these with * Symfony's Application::run() method, but Drush 8 continues to use the * legacy Drush command dispatcher for all commands. * * @return bolean false if command failed (expect drush_set_error was called in this case) */ function annotationcommand_adapter_process_command() { $userArgs = func_get_args(); $commandprocessor = annotationcommand_adapter_get_processor(); $command = drush_get_command(); annotationcommand_adapter_add_hook_options($command); $args = []; foreach ($command['consolidation-arg-defaults'] as $key => $default) { $value = array_shift($userArgs); if (!isset($value)) { $value = $default; } $args[$key] = $value; } $input = new DrushInputAdapter($args, annotationcommand_adapter_get_options($command), $command['command']); $output = new DrushOutputAdapter(); $annotationData = $command['annotations']; $commandData = new CommandData( $annotationData, $input, $output ); $commandData->setIncludeOptionsInArgs($command['add-options-to-arguments']); $names = annotationcommand_adapter_command_names($command); // n.b.: backend result is set by a post-alter hook. $result = $commandprocessor->process( $output, $names, $command['annotated-command-callback'], $commandData ); return $result; } /** * Internal function called by annotationcommand_adapter_commands, which * is called by drush_get_commands(). * * @param array $annotation_commandfiles path => class mapping * * @return object[] */ function annotationcommand_adapter_get_commands($annotation_commandfiles) { $commands = []; // This will give us a list containing something akin to: // 'modules/default_content/src/CliTools/DefaultContentCommands.php' => // '\\Drupal\\default_content\\CliTools\\DefaultContentCommands', foreach ($annotation_commandfiles as $commandfile_path => $commandfile_class) { if (file_exists($commandfile_path)) { $commandhandler = annotationcommand_adapter_create_commandfile_instance($commandfile_path, $commandfile_class); $commands_for_this_commandhandler = annotationcommand_adapter_get_commands_for_commandhandler($commandhandler, $commandfile_path); $commands = array_merge($commands, $commands_for_this_commandhandler); } } return $commands; } /** * Create and cache a commandfile instance. * * @param string $commandfile_path Path to the commandfile implementation * @param string $commandfile_class Namespace and class of the commandfile object * * @return object */ function annotationcommand_adapter_create_commandfile_instance($commandfile_path, $commandfile_class) { $cache =& drush_get_context('DRUSH_ANNOTATION_COMMANDFILE_INSTANCES'); if (!isset($cache[$commandfile_path])) { include_once $commandfile_path; $commandhandler = new $commandfile_class; $cache[$commandfile_path] = $commandhandler; } return $cache[$commandfile_path]; } /** * TODO: document */ function annotationcommand_adapter_cache_module_console_commands($console_command, $commandfile_path = null) { if (!isset($commandfile_path)) { $class = new \ReflectionClass($console_command); $commandfile_path = $class->getFileName(); } $module_service_commands = drush_get_context('DRUSH_MODULE_SERVICE_COMMANDS'); $commands = annotationcommand_adapter_get_command_for_console_command($console_command, $commandfile_path); drush_set_context('DRUSH_MODULE_SERVICE_COMMANDS', array_merge($commands, $module_service_commands)); } /** * TODO: document */ function annotationcommand_adapter_cache_module_service_commands($commandhandler, $commandfile_path = null) { if (!isset($commandfile_path)) { $class = new \ReflectionClass($commandhandler); $commandfile_path = $class->getFileName(); } $module_service_commands = drush_get_context('DRUSH_MODULE_SERVICE_COMMANDS'); $commands = annotationcommand_adapter_get_commands_for_commandhandler($commandhandler, $commandfile_path, false); drush_set_context('DRUSH_MODULE_SERVICE_COMMANDS', array_merge($commands, $module_service_commands)); } /** * Convert a Symfony Console command into a Drush $command record * * @param Symfony\Component\Console\Command\Command $console_command The Symfony Console command to convert * @param string $commandfile_path Path to console command file * * @return array Drush $command record */ function annotationcommand_adapter_get_command_for_console_command($console_command, $commandfile_path) { $commands = []; $commandfile = basename($commandfile_path, '.php'); $factory = annotationcommand_adapter_get_factory(); $inputDefinition = $console_command->getDefinition(); $inputArguments = $inputDefinition->getArguments(); $inputOptions = $inputDefinition->getOptions(); $aliases = $console_command->getAliases(); $command_name = strtolower($console_command->getName()); $standard_alias = str_replace(':', '-', $command_name); if ($command_name != $standard_alias) { $aliases[] = $standard_alias; } $command = [ 'name' => $command_name, 'callback' => 'annotationcommand_adapter_run_console_command', 'drush-console-command' => $console_command, 'commandfile' => $commandfile, 'category' => $commandfile, 'options' => [], 'arguments' => [], 'description' => $console_command->getDescription(), 'examples' => $console_command->getUsages(), 'aliases' => $aliases, ]; foreach ($inputArguments as $arg => $inputArg) { $command['arguments'][$arg] = $inputArg->getDescription(); } $command['required-arguments'] = $inputDefinition->getArgumentRequiredCount(); foreach ($inputOptions as $option => $inputOption) { $description = $inputOption->getDescription(); $default = $inputOption->getDefault(); $command['options'][$option] = ['description' => $description]; if (!empty($default)) { $command['options'][$option]['example-value'] = $default; } } $command += drush_command_defaults($command_name, $commandfile, $commandfile_path); $commands[$command_name] = $command; return $commands; } /** * Convert an annotated command command handler object into a Drush $command record. * * @param object $commandhandler Command handler object * @param string $commandfile_path * @param boolean $includeAllPublicMethods TODO remove this, make it 'false' always * * @return array Drush $command record */ function annotationcommand_adapter_get_commands_for_commandhandler($commandhandler, $commandfile_path, $includeAllPublicMethods = true) { $cache =& drush_get_context('DRUSH_ANNOTATION_COMMANDS_FOR_COMMANDFILE'); if (isset($cache[$commandfile_path])) { return $cache[$commandfile_path]; } $factory = annotationcommand_adapter_get_factory(); $commands = []; $commandfile = basename($commandfile_path, '.php'); $commandinfo_list = $factory->getCommandInfoListFromClass($commandhandler); foreach ($commandinfo_list as $commandinfo) { // Hooks are automatically registered when the commandhandler is // created via registerCommandClass(), so we don't need to do it again here. // $factory->registerCommandHook($commandinfo, $commandhandler); $aliases = $commandinfo->getAliases(); $command_name = strtolower($commandinfo->getName()); $standard_alias = str_replace(':', '-', $command_name); if ($command_name != $standard_alias) { $aliases[] = $standard_alias; } $handle_remote_commands = $commandinfo->getAnnotation('handle-remote-commands') == 'true'; // TODO: if there is no 'bootstrap' annotation, maybe we should default to NONE instead of FULL? if ($bootstrap = $commandinfo->getAnnotation('bootstrap')) { $bootstrap = constant($bootstrap); } $command = [ 'name' => $command_name, //'callback' => [$commandhandler, $commandinfo->getMethodName()], 'callback' => 'annotationcommand_adapter_process_command', 'annotated-command-callback' => [$commandhandler, $commandinfo->getMethodName()], 'commandfile' => $commandfile, 'category' => $commandfile, 'options' => [], 'arguments' => [], 'description' => $commandinfo->getDescription(), 'examples' => $commandinfo->getExampleUsages(), 'bootstrap' => $bootstrap, 'handle-remote-commands' => $handle_remote_commands, 'aliases' => $aliases, 'add-options-to-arguments' => TRUE, 'consolidation-output-formatters' => TRUE, 'consolidation-option-defaults' => $commandinfo->options()->getValues(), 'consolidation-arg-defaults' => $commandinfo->arguments()->getValues(), ]; $required_arguments = 0; foreach ($commandinfo->arguments()->getValues() as $arg => $default) { $command['arguments'][$arg] = $commandinfo->arguments()->getDescription($arg); if (!isset($default)) { ++$required_arguments; } } $command['required-arguments'] = $required_arguments; foreach ($commandinfo->options()->getValues() as $option => $default) { $description = $commandinfo->options()->getDescription($option); $command['options'][$option] = ['description' => $description]; if (!empty($default)) { $command['options'][$option]['example-value'] = $default; } $fn = 'annotationcommand_adapter_alter_option_description_' . $option; if (function_exists($fn)) { $command['options'][$option] = $fn($command['options'][$option], $commandinfo, $default); } } $command['annotations'] = $commandinfo->getAnnotations(); // If the command has a '@return' annotation, then // remember information we will need to use the output formatter. $returnType = $commandinfo->getReturnType(); if (isset($returnType)) { $command['return-type'] = $returnType; } $command += drush_command_defaults($command_name, $commandfile, $commandfile_path); $commands[$command_name] = $command; } $cache[$commandfile_path] = $commands; return $commands; } /** * Modify a $command record, adding option definitions defined by any * command hook. * * @param array $command Drush command record to modify */ function annotationcommand_adapter_add_hook_options(&$command) { // Get options added by hooks. We postpone doing this until the // last minute rather than doing it when processing commandfiles // so that we do not need to worry about what order we process the // commandfiles in -- we can load extensions late, and still have // the extension hook a core command, or have an early-loaded global // extension hook a late-loaded extension (e.g. attached to a module). $names = annotationcommand_adapter_command_names($command); $names[] = '*'; // we are missing annotations here; maybe we just don't support that? (TODO later, maybe) $factory = annotationcommand_adapter_get_factory(); $extraOptions = $factory->hookManager()->getHookOptions($names); foreach ($extraOptions as $commandinfo) { if (!isset($command['consolidation-option-defaults'])) { $command['consolidation-option-defaults'] = array(); } $command['consolidation-option-defaults'] += $commandinfo->options()->getValues(); foreach ($commandinfo->options()->getValues() as $option => $default) { $description = $commandinfo->options()->getDescription($option); $command['options'][$option] = ['description' => $description]; if (!empty($default)) { $command['options'][$option]['example-value'] = $default; } $fn = 'annotationcommand_adapter_alter_option_description_' . $option; if (function_exists($fn)) { $command['options'][$option] = $fn($command['options'][$option], $commandinfo, $default); } } } } /** * Build all of the name variants for a Drush $command record * * @param array $command Drush command record * * @return string[] */ function annotationcommand_adapter_command_names($command) { $names = array_merge( [$command['command']], $command['aliases'] ); if (!empty($command['annotated-command-callback'])) { $commandHandler = $command['annotated-command-callback'][0]; $reflectionClass = new \ReflectionClass($commandHandler); $commandFileClass = $reflectionClass->getName(); $names[] = $commandFileClass; } return $names; } /** * Convert from an old-style Drush initialize hook into annotated-command hooks. * @see _drush_invoke_hooks(). * * @param string[] $names All of the applicable names for the command being hooked * @param CommandData $commandData All of the parameter data associated with the * current command invokation, including the InputInterface, OutputInterface * and AnnotationData */ function annotationcommand_adapter_call_initialize($names, CommandData $commandData) { $factory = annotationcommand_adapter_get_factory(); $hookManager = $factory->hookManager(); $hooks = $hookManager->getHooks( $names, [ HookManager::PRE_INITIALIZE, HookManager::INITIALIZE, HookManager::POST_INITIALIZE, ], $commandData->annotationData() ); foreach ((array)$hooks as $hook) { $hook($commandData->input(), $commandData->annotationData()); } } /** * Convert from an old-style Drush pre-validate hook into annotated-command hooks. * @see _drush_invoke_hooks(). * * @param string[] $names All of the applicable names for the command being hooked * @param CommandData $commandData All of the parameter data associated with the * current command invokation, including the InputInterface, OutputInterface * and AnnotationData * * @return boolean|object */ function annotationcommand_adapter_call_hook_pre_validate($names, CommandData $commandData) { return annotationcommand_adapter_call_validate_interface( $names, [ HookManager::PRE_ARGUMENT_VALIDATOR, ], $commandData ); } /** * Convert from an old-style Drush validate hook into annotated-command hooks. * @see _drush_invoke_hooks(). * * @param string[] $names All of the applicable names for the command being hooked * @param CommandData $commandData All of the parameter data associated with the * current command invokation, including the InputInterface, OutputInterface * and AnnotationData * * @return boolean|object */ function annotationcommand_adapter_call_hook_validate($names, CommandData $commandData) { return annotationcommand_adapter_call_validate_interface( $names, [ HookManager::ARGUMENT_VALIDATOR, ], $commandData ); } /** * Convert from an old-style Drush pre-command hook into annotated-command hooks. * @see _drush_invoke_hooks(). * * @param string[] $names All of the applicable names for the command being hooked * @param CommandData $commandData All of the parameter data associated with the * current command invokation, including the InputInterface, OutputInterface * and AnnotationData * * @return boolean|object */ function annotationcommand_adapter_call_hook_pre_command($names, CommandData $commandData) { return annotationcommand_adapter_call_validate_interface( $names, [ HookManager::PRE_COMMAND_HOOK, ], $commandData ); } /** * Convert from an old-style Drush 'command' hook into annotated-command hooks. * @see _drush_invoke_hooks(). * * @param string[] $names All of the applicable names for the command being hooked * @param CommandData $commandData All of the parameter data associated with the * current command invokation, including the InputInterface, OutputInterface * and AnnotationData * * @return boolean|object */ function annotationcommand_adapter_call_hook_command($names, CommandData $commandData) { return annotationcommand_adapter_call_validate_interface( $names, [ HookManager::COMMAND_HOOK, ], $commandData ); } /** * Convert from an old-style Drush post-command hook into annotated-command hooks. * @see _drush_invoke_hooks(). * * @param string[] $names All of the applicable names for the command being hooked * @param CommandData $commandData All of the parameter data associated with the * current command invokation, including the InputInterface, OutputInterface * and AnnotationData * @param mixed $return The return value of the command being executed * * @return mixed The altered command return value */ function annotationcommand_adapter_call_hook_post_command($names, CommandData $commandData, $return) { return annotationcommand_adapter_call_process_interface( $names, [ HookManager::POST_COMMAND_HOOK, ], $commandData, $return ); } /** * After the primary Drush command hook is called, call all of the annotated-command * process and alter hooks. * @see _drush_invoke_hooks(). * * @param string[] $names All of the applicable names for the command being hooked * @param CommandData $commandData All of the parameter data associated with the * current command invokation, including the InputInterface, OutputInterface * and AnnotationData * @param mixed $return The return value of the command being executed * * @return mixed The altered command return value */ function annotationcommand_adapter_call_hook_process_and_alter($names, $commandData, $return) { return annotationcommand_adapter_call_process_interface( $names, [ HookManager::PRE_PROCESS_RESULT, HookManager::PROCESS_RESULT, HookManager::POST_PROCESS_RESULT, HookManager::PRE_ALTER_RESULT, HookManager::ALTER_RESULT, HookManager::POST_ALTER_RESULT, ], $commandData, $return ); } /** * Given a list of hooks that conform to the interface ProcessResultInterface, * call them and return the result. * * @param string[] $names All of the applicable names for the command being hooked * @param string[] $hooks All of the HookManager hooks that should be called * @param CommandData $commandData All of the parameter data associated with the * current command invokation, including the InputInterface, OutputInterface * and AnnotationData * @param mixed $return The return value of the command being executed * * @return mixed The altered command return value */ function annotationcommand_adapter_call_process_interface($names, $hooks, CommandData $commandData, $return) { $factory = annotationcommand_adapter_get_factory(); $hookManager = $factory->hookManager(); $hooks = $hookManager->getHooks($names, $hooks, $commandData->annotationData()); foreach ((array)$hooks as $hook) { $result = $hook($return, $commandData); if (isset($result)) { $return = $result; } } return $return; } /** * Given a list of hooks that conform to the interface ValidatorInterface, * call them and return the result. * * @param string[] $names All of the applicable names for the command being hooked * @param string[] $hooks All of the HookManager hooks that should be called * @param CommandData $commandData All of the parameter data associated with the * current command invokation, including the InputInterface, OutputInterface * and AnnotationData * * @return boolean|object */ function annotationcommand_adapter_call_validate_interface($names, $hooks, CommandData $commandData) { $factory = annotationcommand_adapter_get_factory(); $hookManager = $factory->hookManager(); $annotationData = $commandData->annotationData(); $hooks = $hookManager->getHooks($names, $hooks, $annotationData); foreach ((array)$hooks as $hook) { $validated = $hook($commandData); // TODO: if $validated is a CommandError, maybe the best thing to do is 'return drush_set_error()'? if (is_object($validated) || ($validated === false)) { return $validated; } } return true; } /** * TODO: Document */ function annotationcommand_adapter_alter_option_description_format($option_help, $commandinfo, $default) { $formatterManager = annotatedcomand_adapter_get_formatter(); $return_type = $commandinfo->getReturnType(); if (!empty($return_type)) { $available_formats = $formatterManager->validFormats($return_type); $option_help['description'] = dt('Select output format. Available: !formats.', array('!formats' => implode(', ', $available_formats))); if (!empty($default)) { $option_help['description'] .= dt(' Default is !default.', array('!default' => $default)); } } return $option_help; } /** * TODO: Document */ function annotationcommand_adapter_alter_option_description_fields($option_help, $commandinfo, $default) { $formatOptions = new FormatterOptions($commandinfo->getAnnotations()->getArrayCopy()); $field_labels = $formatOptions->get(FormatterOptions::FIELD_LABELS); $default_fields = $formatOptions->get(FormatterOptions::DEFAULT_FIELDS, [], array_keys($field_labels)); $available_fields = array_keys($field_labels); $option_help['example-value'] = implode(', ', $default_fields); $option_help['description'] = dt('Fields to output. All available fields are: !available.', array('!available' => implode(', ', $available_fields))); return $option_help; } /** * In some circumstances, Drush just does a deep search for any *.drush.inc * file, so that it can find all commands, in enabled and disabled modules alike, * for the purpose of displaying the help text for that command. */ function annotationcommand_adapter_refine_searchpaths($searchpath) { $result = []; foreach ($searchpath as $path) { $max_depth = TRUE; $pattern = '/.*\.info$/'; if (drush_drupal_major_version() > 7) { $pattern = '/.*\.info.yml$/'; } $locations = drush_scan_directory($path, $pattern, ['.', '..'], false, $max_depth); // Search for any directory that might be a module or theme (contains // a *.info or a *.info.yml file) foreach ($locations as $key => $info) { $result[dirname($key)] = true; } } return array_keys($result); } >>'); define('DRUSH_BACKEND_OUTPUT_DELIMITER', DRUSH_BACKEND_OUTPUT_START . '%s<< "\\0")); $packet_regex = str_replace("\n", "", $packet_regex); $data['output'] = preg_replace("/$packet_regex/s", '', drush_backend_output_collect(NULL)); } if (drush_get_context('DRUSH_QUIET', FALSE)) { ob_end_clean(); } $result_object = drush_backend_get_result(); if (isset($result_object)) { $data['object'] = $result_object; } $error = drush_get_error(); $data['error_status'] = ($error) ? $error : DRUSH_SUCCESS; $data['log'] = drush_get_log(); // Append logging information // The error log is a more specific version of the log, and may be used by calling // scripts to check for specific errors that have occurred. $data['error_log'] = drush_get_error_log(); // If there is a @self record, then include it in the result $self_record = drush_sitealias_get_record('@self'); if (!empty($self_record)) { $site_context = drush_get_context('site', array()); unset($site_context['config-file']); unset($site_context['context-path']); unset($self_record['loaded-config']); unset($self_record['#name']); $data['self'] = array_merge($site_context, $self_record); } // Return the options that were set at the end of the process. $data['context'] = drush_get_merged_options(); printf("\0" . DRUSH_BACKEND_OUTPUT_DELIMITER, json_encode($data)); } /** * Callback to collect backend command output. */ function drush_backend_output_collect($string) { static $output = ''; if (!isset($string)) { return $output; } $output .= $string; return $string; } /** * Output buffer functions that discards all output but backend packets. */ function drush_backend_output_discard($string) { $packet_regex = strtr(sprintf(DRUSH_BACKEND_PACKET_PATTERN, "([^\0]*)"), array("\0" => "\\0")); $packet_regex = str_replace("\n", "", $packet_regex); if (preg_match_all("/$packet_regex/s", $string, $matches)) { return implode('', $matches[0]); } } /** * Output a backend packet if we're running as backend. * * @param packet * The packet to send. * @param data * Data for the command. * * @return * A boolean indicating whether the command was output. */ function drush_backend_packet($packet, $data) { if (drush_get_context('DRUSH_BACKEND')) { $data['packet'] = $packet; $data = json_encode($data); // We use 'fwrite' instead of 'drush_print' here because // this backend packet is out-of-band data. fwrite(STDERR, sprintf(DRUSH_BACKEND_PACKET_PATTERN, $data)); return TRUE; } return FALSE; } /** * Parse output returned from a Drush command. * * @param string * The output of a drush command * @param integrate * Integrate the errors and log messages from the command into the current process. * @param outputted * Whether output has already been handled. * * @return * An associative array containing the data from the external command, or the string parameter if it * could not be parsed successfully. */ function drush_backend_parse_output($string, $backend_options = array(), $outputted = FALSE) { $regex = sprintf(DRUSH_BACKEND_OUTPUT_DELIMITER, '(.*)'); preg_match("/$regex/s", $string, $match); if (!empty($match) && $match[1]) { // we have our JSON encoded string $output = $match[1]; // remove the match we just made and any non printing characters $string = trim(str_replace(sprintf(DRUSH_BACKEND_OUTPUT_DELIMITER, $match[1]), '', $string)); } if (!empty($output)) { $data = json_decode($output, TRUE); if (is_array($data)) { _drush_backend_integrate($data, $backend_options, $outputted); return $data; } } return $string; } /** * Integrate log messages and error statuses into the current * process. * * Output produced by the called script will be printed if we didn't print it * on the fly, errors will be set, and log messages will be logged locally, if * not already logged. * * @param data * The associative array returned from the external command. * @param outputted * Whether output has already been handled. */ function _drush_backend_integrate($data, $backend_options, $outputted) { // In 'integrate' mode, logs and errors have already been handled // by drush_backend_packet (sender) drush_backend_parse_packets (receiver - us) // during incremental output. We therefore do not need to call drush_set_error // or drush_log here. The exception is if the sender is an older version of // Drush (version 4.x) that does not send backend packets, then we will // not have processed the log entries yet, and must print them here. $received_packets = drush_get_context('DRUSH_RECEIVED_BACKEND_PACKETS', FALSE); if (is_array($data['log']) && $backend_options['log'] && (!$received_packets)) { foreach($data['log'] as $log) { $message = is_array($log['message']) ? implode("\n", $log['message']) : $log['message']; if (isset($backend_options['#output-label'])) { $message = $backend_options['#output-label'] . $message; } if (isset($log['error']) && $backend_options['integrate']) { drush_set_error($log['error'], $message); } elseif ($backend_options['integrate']) { drush_log($message, $log['type']); } } } // Output will either be printed, or buffered to the drush_backend_output command. // If the output has already been printed, then we do not need to show it again on a failure. if (!$outputted) { if (drush_cmp_error('DRUSH_APPLICATION_ERROR') && !empty($data['output'])) { drush_set_error("DRUSH_APPLICATION_ERROR", dt("Output from failed command :\n !output", array('!output' => $data['output']))); } elseif ($backend_options['output']) { _drush_backend_print_output($data['output'], $backend_options); } } } /** * Supress log message output during backend integrate. */ function _drush_backend_integrate_log($entry) { } /** * Call an external command using proc_open. * * @param cmds * An array of records containing the following elements: * 'cmd' - The command to execute, already properly escaped * 'post-options' - An associative array that will be JSON encoded * and passed to the script being called. Objects are not allowed, * as they do not json_decode gracefully. * 'backend-options' - Options that control the operation of the backend invoke * - OR - * An array of commands to execute. These commands already need to be properly escaped. * In this case, post-options will default to empty, and a default output label will * be generated. * @param data * An associative array that will be JSON encoded and passed to the script being called. * Objects are not allowed, as they do not json_decode gracefully. * * @return * False if the command could not be executed, or did not return any output. * If it executed successfully, it returns an associative array containing the command * called, the output of the command, and the error code of the command. */ function _drush_backend_proc_open($cmds, $process_limit, $context = NULL) { $descriptorspec = array( 0 => array("pipe", "r"), // stdin is a pipe that the child will read from 1 => array("pipe", "w"), // stdout is a pipe that the child will write to ); $open_processes = array(); $bucket = array(); $process_limit = max($process_limit, 1); $is_windows = drush_is_windows(); // Loop through processes until they all close, having a nap as needed. $nap_time = 0; while (count($open_processes) || count($cmds)) { $nap_time++; if (count($cmds) && (count($open_processes) < $process_limit)) { // Pop the site and command (key / value) from the cmds array end($cmds); list($site, $cmd) = each($cmds); unset($cmds[$site]); if (is_array($cmd)) { $c = $cmd['cmd']; $post_options = $cmd['post-options']; $backend_options = $cmd['backend-options']; } else { $c = $cmd; $post_options = array(); $backend_options = array(); } $backend_options += array( '#output-label' => '', '#process-read-size' => 4096, ); $process = array(); drush_log($backend_options['#output-label'] . $c); $process['process'] = proc_open($c, $descriptorspec, $process['pipes'], null, null, array('context' => $context)); if (is_resource($process['process'])) { if ($post_options) { fwrite($process['pipes'][0], json_encode($post_options)); // pass the data array in a JSON encoded string } // If we do not close stdin here, then we cause a deadlock; // see: http://drupal.org/node/766080#comment-4309936 // If we reimplement interactive commands to also use // _drush_proc_open, then clearly we would need to keep // this open longer. fclose($process['pipes'][0]); $process['info'] = stream_get_meta_data($process['pipes'][1]); stream_set_blocking($process['pipes'][1], FALSE); stream_set_timeout($process['pipes'][1], 1); $bucket[$site]['cmd'] = $c; $bucket[$site]['output'] = ''; $bucket[$site]['remainder'] = ''; $bucket[$site]['backend-options'] = $backend_options; $bucket[$site]['end_of_output'] = FALSE; $bucket[$site]['outputted'] = FALSE; $open_processes[$site] = $process; } // Reset the $nap_time variable as there might be output to process next // time around: $nap_time = 0; } // Set up to call stream_select(). See: // http://php.net/manual/en/function.stream-select.php // We can't use stream_select on Windows, because it doesn't work for // streams returned by proc_open. if (!$is_windows) { $ss_result = 0; $read_streams = array(); $write_streams = array(); $except_streams = array(); foreach ($open_processes as $site => &$current_process) { if (isset($current_process['pipes'][1])) { $read_streams[] = $current_process['pipes'][1]; } } // Wait up to 2s for data to become ready on one of the read streams. if (count($read_streams)) { $ss_result = stream_select($read_streams, $write_streams, $except_streams, 2); // If stream_select returns a error, then fallback to using $nap_time. if ($ss_result !== FALSE) { $nap_time = 0; } } } foreach ($open_processes as $site => &$current_process) { if (isset($current_process['pipes'][1])) { // Collect output from stdout $bucket[$site][1] = ''; $info = stream_get_meta_data($current_process['pipes'][1]); if (!feof($current_process['pipes'][1]) && !$info['timed_out']) { $string = $bucket[$site]['remainder'] . fread($current_process['pipes'][1], $backend_options['#process-read-size']); $bucket[$site]['remainder'] = ''; $output_end_pos = strpos($string, DRUSH_BACKEND_OUTPUT_START); if ($output_end_pos !== FALSE) { $trailing_string = substr($string, 0, $output_end_pos); $trailing_remainder = ''; // If there is any data in the trailing string (characters prior // to the backend output start), then process any backend packets // embedded inside. if (strlen($trailing_string) > 0) { drush_backend_parse_packets($trailing_string, $trailing_remainder, $bucket[$site]['backend-options']); } // If there is any data remaining in the trailing string after // the backend packets are removed, then print it. if (strlen($trailing_string) > 0) { _drush_backend_print_output($trailing_string . $trailing_remainder, $bucket[$site]['backend-options']); $bucket[$site]['outputted'] = TRUE; } $bucket[$site]['end_of_output'] = TRUE; } if (!$bucket[$site]['end_of_output']) { drush_backend_parse_packets($string, $bucket[$site]['remainder'], $bucket[$site]['backend-options']); // Pass output through. _drush_backend_print_output($string, $bucket[$site]['backend-options']); if (strlen($string) > 0) { $bucket[$site]['outputted'] = TRUE; } } $bucket[$site][1] .= $string; $bucket[$site]['output'] .= $string; $info = stream_get_meta_data($current_process['pipes'][1]); flush(); // Reset the $nap_time variable as there might be output to process // next time around: if (strlen($string) > 0) { $nap_time = 0; } } else { fclose($current_process['pipes'][1]); unset($current_process['pipes'][1]); // close the pipe , set a marker // Reset the $nap_time variable as there might be output to process // next time around: $nap_time = 0; } } else { // if both pipes are closed for the process, remove it from active loop and add a new process to open. $bucket[$site]['code'] = proc_close($current_process['process']); unset($open_processes[$site]); // Reset the $nap_time variable as there might be output to process next // time around: $nap_time = 0; } } // We should sleep for a bit if we need to, up to a maximum of 1/10 of a // second. if ($nap_time > 0) { usleep(max($nap_time * 500, 100000)); } } return $bucket; // TODO: Handle bad proc handles //} //return FALSE; } /** * Print the output received from a call to backend invoke, * adding the label to the head of each line if necessary. */ function _drush_backend_print_output($output_string, $backend_options) { if ($backend_options['output'] && !empty($output_string)) { $output_label = array_key_exists('#output-label', $backend_options) ? $backend_options['#output-label'] : FALSE; if (!empty($output_label)) { // Remove one, and only one newline from the end of the // string. Else we'll get an extra 'empty' line. foreach (explode("\n", preg_replace('/\\n$/', '', $output_string)) as $line) { fwrite(STDOUT, $output_label . rtrim($line) . "\n"); } } else { fwrite(STDOUT, $output_string); } } } /** * Parse out and remove backend packet from the supplied string and * invoke the commands. */ function drush_backend_parse_packets(&$string, &$remainder, $backend_options) { $remainder = ''; $packet_regex = strtr(sprintf(DRUSH_BACKEND_PACKET_PATTERN, "([^\0]*)"), array("\0" => "\\0")); $packet_regex = str_replace("\n", "", $packet_regex); if (preg_match_all("/$packet_regex/s", $string, $match, PREG_PATTERN_ORDER)) { drush_set_context('DRUSH_RECEIVED_BACKEND_PACKETS', TRUE); foreach ($match[1] as $packet_data) { $entry = (array) json_decode($packet_data); if (is_array($entry) && isset($entry['packet'])) { $function = 'drush_backend_packet_' . $entry['packet']; if (function_exists($function)) { $function($entry, $backend_options); } else { drush_log(dt("Unknown backend packet @packet", array('@packet' => $entry['packet'])), LogLevel::NOTICE); } } else { drush_log(dt("Malformed backend packet"), LogLevel::ERROR); drush_log(dt("Bad packet: @packet", array('@packet' => print_r($entry, TRUE))), LogLevel::DEBUG); drush_log(dt("String is: @str", array('@str' => $packet_data), LogLevel::DEBUG)); } } $string = preg_replace("/$packet_regex/s", '', $string); } // Check to see if there is potentially a partial packet remaining. // We only care about the last null; if there are any nulls prior // to the last one, they would have been removed above if they were // valid drush packets. $embedded_null = strrpos($string, "\0"); if ($embedded_null !== FALSE) { // We will consider everything after $embedded_null to be part of // the $remainder string if: // - the embedded null is less than strlen(DRUSH_BACKEND_OUTPUT_START) // from the end of $string (that is, there might be a truncated // backend packet header, or the truncated backend output start // after the null) // OR // - the embedded null is followed by DRUSH_BACKEND_PACKET_START // (that is, the terminating null for that packet has not been // read into our buffer yet) if (($embedded_null + strlen(DRUSH_BACKEND_OUTPUT_START) >= strlen($string)) || (substr($string, $embedded_null + 1, strlen(DRUSH_BACKEND_PACKET_START)) == DRUSH_BACKEND_PACKET_START)) { $remainder = substr($string, $embedded_null); $string = substr($string, 0, $embedded_null); } } } /** * Backend command for setting errors. */ function drush_backend_packet_set_error($data, $backend_options) { if (!$backend_options['integrate']) { return; } $output_label = ""; if (array_key_exists('#output-label', $backend_options)) { $output_label = $backend_options['#output-label']; } drush_set_error($data['error'], $data['message'], $output_label); } /** * Default options for backend_invoke commands. */ function _drush_backend_adjust_options($site_record, $command, $command_options, $backend_options) { // By default, if the caller does not specify a value for 'output', but does // specify 'integrate' === FALSE, then we will set output to FALSE. Otherwise we // will allow it to default to TRUE. if ((array_key_exists('integrate', $backend_options)) && ($backend_options['integrate'] === FALSE) && (!array_key_exists('output', $backend_options))) { $backend_options['output'] = FALSE; } $has_site_specification = array_key_exists('root', $site_record) || array_key_exists('uri', $site_record); $result = $backend_options + array( 'method' => 'GET', 'output' => TRUE, 'log' => TRUE, 'integrate' => TRUE, 'backend' => TRUE, 'dispatch-using-alias' => !$has_site_specification, ); // Convert '#integrate' et. al. into backend options foreach ($command_options as $key => $value) { if (substr($key,0,1) === '#') { $result[substr($key,1)] = $value; } } return $result; } /** * Execute a new local or remote command in a new process. * * n.b. Prefer drush_invoke_process() to this function. * * @param invocations * An array of command records to exacute. Each record should contain: * 'site': * An array containing information used to generate the command. * 'remote-host' * Optional. A remote host to execute the drush command on. * 'remote-user' * Optional. Defaults to the current user. If you specify this, you can choose which module to send. * 'ssh-options' * Optional. Defaults to "-o PasswordAuthentication=no" * '#env-vars' * Optional. An associative array of environmental variables to prefix the Drush command with. * 'path-aliases' * Optional; contains paths to folders and executables useful to the command. * '%drush-script' * Optional. Defaults to the current drush.php file on the local machine, and * to simply 'drush' (the drush script in the current PATH) on remote servers. * You may also specify a different drush.php script explicitly. You will need * to set this when calling drush on a remote server if 'drush' is not in the * PATH on that machine. * 'command': * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'. * 'args': * An array of arguments for the command. * 'options' * Optional. An array containing options to pass to the remote script. * Array items with a numeric key are treated as optional arguments to the * command. * 'backend-options': * Optional. Additional parameters that control the operation of the invoke. * 'method' * Optional. Defaults to 'GET'. * If this parameter is set to 'POST', the $data array will be passed * to the script being called as a JSON encoded string over the STDIN * pipe of that process. This is preferable if you have to pass * sensitive data such as passwords and the like. * For any other value, the $data array will be collapsed down into a * set of command line options to the script. * 'integrate' * Optional. Defaults to TRUE. * If TRUE, any error statuses will be integrated into the current * process. This might not be what you want, if you are writing a * command that operates on multiple sites. * 'log' * Optional. Defaults to TRUE. * If TRUE, any log messages will be integrated into the current * process. * 'output' * Optional. Defaults to TRUE. * If TRUE, output from the command will be synchronously printed to * stdout. * 'drush-script' * Optional. Defaults to the current drush.php file on the local * machine, and to simply 'drush' (the drush script in the current * PATH) on remote servers. You may also specify a different drush.php * script explicitly. You will need to set this when calling drush on * a remote server if 'drush' is not in the PATH on that machine. * 'dispatch-using-alias' * Optional. Defaults to FALSE. * If specified as a non-empty value the drush command will be * dispatched using the alias name on the command line, instead of * the options from the alias being added to the command line * automatically. * @param common_options * Optional. Merged in with the options for each invocation. * @param backend_options * Optional. Merged in with the backend options for each invocation. * @param default_command * Optional. Used as the 'command' for any invocation that does not * define a command explicitly. * @param default_site * Optional. Used as the 'site' for any invocation that does not * define a site explicitly. * @param context * Optional. Passed in to proc_open if provided. * * @return * If the command could not be completed successfully, FALSE. * If the command was completed, this will return an associative array containing the data from drush_backend_output(). */ function drush_backend_invoke_concurrent($invocations, $common_options = array(), $common_backend_options = array(), $default_command = NULL, $default_site = NULL, $context = NULL) { $index = 0; // Slice and dice our options in preparation to build a command string $invocation_options = array(); foreach ($invocations as $invocation) { $site_record = isset($invocation['site']) ? $invocation['site'] : $default_site; // NULL is a synonym to '@self', although the latter is preferred. if (!isset($site_record)) { $site_record = '@self'; } // If the first parameter is not a site alias record, // then presume it is an alias name, and try to look up // the alias record. if (!is_array($site_record)) { $site_record = drush_sitealias_get_record($site_record); } $command = isset($invocation['command']) ? $invocation['command'] : $default_command; $args = isset($invocation['args']) ? $invocation['args'] : array(); $command_options = isset($invocation['options']) ? $invocation['options'] : array(); $backend_options = isset($invocation['backend-options']) ? $invocation['backend-options'] : array(); // If $backend_options is passed in as a bool, interpret that as the value for 'integrate' if (!is_array($common_backend_options)) { $integrate = (bool)$common_backend_options; $common_backend_options = array('integrate' => $integrate); } $command_options += $common_options; $backend_options += $common_backend_options; $backend_options = _drush_backend_adjust_options($site_record, $command, $command_options, $backend_options); $backend_options += array( 'drush-script' => NULL, ); // Insure that contexts such as DRUSH_SIMULATE and NO_COLOR are included. $command_options += _drush_backend_get_global_contexts($site_record); // Add in command-specific options as well $command_options += drush_command_get_command_specific_options($site_record, $command); // If the caller has requested it, don't pull the options from the alias // into the command line, but use the alias name for dispatching. if (!empty($backend_options['dispatch-using-alias']) && isset($site_record['#name'])) { list($post_options, $commandline_options, $drush_global_options) = _drush_backend_classify_options(array(), $command_options, $backend_options); $site_record_to_dispatch = '@' . ltrim($site_record['#name'], '@'); } else { list($post_options, $commandline_options, $drush_global_options) = _drush_backend_classify_options($site_record, $command_options, $backend_options); $site_record_to_dispatch = ''; } if (array_key_exists('backend-simulate', $backend_options)) { $drush_global_options['simulate'] = TRUE; } $site_record += array('path-aliases' => array(), '#env-vars' => array()); $site_record['path-aliases'] += array( '%drush-script' => $backend_options['drush-script'], ); $site = (array_key_exists('#name', $site_record) && !array_key_exists($site_record['#name'], $invocation_options)) ? $site_record['#name'] : $index++; $invocation_options[$site] = array( 'site-record' => $site_record, 'site-record-to-dispatch' => $site_record_to_dispatch, 'command' => $command, 'args' => $args, 'post-options' => $post_options, 'drush-global-options' => $drush_global_options, 'commandline-options' => $commandline_options, 'command-options' => $command_options, 'backend-options' => $backend_options, ); } // Calculate the length of the longest output label $max_name_length = 0; $label_separator = ''; if (!array_key_exists('no-label', $common_options) && (count($invocation_options) > 1)) { $label_separator = array_key_exists('label-separator', $common_options) ? $common_options['label-separator'] : ' >> '; foreach ($invocation_options as $site => $item) { $backend_options = $item['backend-options']; if (!array_key_exists('#output-label', $backend_options)) { if (is_numeric($site)) { $backend_options['#output-label'] = ' * [@self.' . $site; $label_separator = '] '; } else { $backend_options['#output-label'] = $site; } $invocation_options[$site]['backend-options']['#output-label'] = $backend_options['#output-label']; } $name_len = strlen($backend_options['#output-label']); if ($name_len > $max_name_length) { $max_name_length = $name_len; } if (array_key_exists('#label-separator', $backend_options)) { $label_separator = $backend_options['#label-separator']; } } } // Now pad out the output labels and add the label separator. $reserve_margin = $max_name_length + strlen($label_separator); foreach ($invocation_options as $site => $item) { $backend_options = $item['backend-options'] + array('#output-label' => ''); $invocation_options[$site]['backend-options']['#output-label'] = str_pad($backend_options['#output-label'], $max_name_length, " ") . $label_separator; if ($reserve_margin) { $invocation_options[$site]['drush-global-options']['reserve-margin'] = $reserve_margin; } } // Now take our prepared options and generate the command strings $cmds = array(); foreach ($invocation_options as $site => $item) { $site_record = $item['site-record']; $site_record_to_dispatch = $item['site-record-to-dispatch']; $command = $item['command']; $args = $item['args']; $post_options = $item['post-options']; $commandline_options = $item['commandline-options']; $command_options = $item['command-options']; $drush_global_options = $item['drush-global-options']; $backend_options = $item['backend-options']; $is_remote = array_key_exists('remote-host', $site_record); $is_different_site = $is_remote || (isset($site_record['root']) && ($site_record['root'] != drush_get_context('DRUSH_DRUPAL_ROOT'))) || (isset($site_record['uri']) && ($site_record['uri'] != drush_get_context('DRUSH_SELECTED_URI'))); $os = drush_os($site_record); // If the caller did not pass in a specific path to drush, then we will // use a default value. For commands that are being executed on the same // machine, we will use DRUSH_COMMAND, which is the path to the drush.php // that is running right now. For remote commands, we will run a wrapper // script instead of drush.php called drush. $drush_path = $site_record['path-aliases']['%drush-script']; if (!$drush_path && !$is_remote && $is_different_site) { $drush_path = find_wrapper_or_launcher($site_record['root']); } $env_vars = $site_record['#env-vars']; $php = array_key_exists('php', $site_record) ? $site_record['php'] : (array_key_exists('php', $command_options) ? $command_options['php'] : NULL); $drush_command_path = drush_build_drush_command($drush_path, $php, $os, $is_remote, $env_vars); $cmd = _drush_backend_generate_command($site_record, $drush_command_path . " " . _drush_backend_argument_string($drush_global_options, $os) . " " . $site_record_to_dispatch . " " . $command, $args, $commandline_options, $backend_options) . ' 2>&1'; $cmds[$site] = array( 'cmd' => $cmd, 'post-options' => $post_options, 'backend-options' => $backend_options, ); } return _drush_backend_invoke($cmds, $common_backend_options, $context); } /** * Find all of the drush contexts that are used to cache global values and * return them in an associative array. */ function _drush_backend_get_global_contexts($site_record) { $result = array(); $global_option_list = drush_get_global_options(FALSE); foreach ($global_option_list as $global_key => $global_metadata) { if (is_array($global_metadata)) { $value = ''; if (!array_key_exists('never-propagate', $global_metadata)) { if ((array_key_exists('propagate', $global_metadata))) { $value = drush_get_option($global_key); } elseif ((array_key_exists('propagate-cli-value', $global_metadata))) { $value = drush_get_option($global_key, '', 'cli'); } elseif ((array_key_exists('context', $global_metadata))) { // If the context is declared to be a 'local-context-only', // then only put it in if this is a local dispatch. if (!array_key_exists('local-context-only', $global_metadata) || !array_key_exists('remote-host', $site_record)) { $value = drush_get_context($global_metadata['context'], array()); } } if (!empty($value) || ($value === '0')) { $result[$global_key] = $value; } } } } return $result; } /** * Take all of the values in the $command_options array, and place each of * them into one of the following result arrays: * * - $post_options: options to be encoded as JSON and written to the * standard input of the drush subprocess being executed. * - $commandline_options: options to be placed on the command line of the drush * subprocess. * - $drush_global_options: the drush global options also go on the command * line, but appear before the drush command name rather than after it. * * Also, this function may modify $backend_options. */ function _drush_backend_classify_options($site_record, $command_options, &$backend_options) { // In 'POST' mode (the default, remove everything (except the items marked 'never-post' // in the global option list) from the commandline options and put them into the post options. // The post options will be json-encoded and sent to the command via stdin $global_option_list = drush_get_global_options(FALSE); // These should be in the command line. $additional_global_options = array(); if (array_key_exists('additional-global-options', $backend_options)) { $additional_global_options = $backend_options['additional-global-options']; $command_options += $additional_global_options; } $method_post = ((!array_key_exists('method', $backend_options)) || ($backend_options['method'] == 'POST')); $post_options = array(); $commandline_options = array(); $drush_global_options = array(); $drush_local_options = array(); $additional_backend_options = array(); foreach ($site_record as $key => $value) { if (!in_array($key, drush_sitealias_site_selection_keys())) { if ($key[0] == '#') { $backend_options[$key] = $value; } if (!isset($command_options[$key])) { if (array_key_exists($key, $global_option_list)) { $command_options[$key] = $value; } } } } if (array_key_exists('drush-local-options', $backend_options)) { $drush_local_options = $backend_options['drush-local-options']; $command_options += $drush_local_options; } if (!empty($backend_options['backend']) && empty($backend_options['interactive']) && empty($backend_options['fork'])) { $drush_global_options['backend'] = '2'; } foreach ($command_options as $key => $value) { $global = array_key_exists($key, $global_option_list); $propagate = TRUE; $special = FALSE; if ($global) { $propagate = (!array_key_exists('never-propagate', $global_option_list[$key])); $special = (array_key_exists('never-post', $global_option_list[$key])); if ($propagate) { // We will allow 'merge-pathlist' contexts to be propogated. Right now // these are all 'local-context-only' options; if we allowed them to // propogate remotely, then we would need to get the right path separator // for the remote machine. if (is_array($value) && array_key_exists('merge-pathlist', $global_option_list[$key])) { $value = implode(PATH_SEPARATOR, $value); } } } // Just remove options that are designated as non-propagating if ($propagate === TRUE) { // In METHOD POST, move command options to post options if ($method_post && ($special === FALSE)) { $post_options[$key] = $value; } // In METHOD GET, ignore options with array values elseif (!is_array($value)) { if ($global || array_key_exists($key, $additional_global_options)) { $drush_global_options[$key] = $value; } else { $commandline_options[$key] = $value; } } } } return array($post_options, $commandline_options, $drush_global_options, $additional_backend_options); } /** * Create a new pipe with proc_open, and attempt to parse the output. * * We use proc_open instead of exec or others because proc_open is best * for doing bi-directional pipes, and we need to pass data over STDIN * to the remote script. * * Exec also seems to exhibit some strangeness in keeping the returned * data intact, in that it modifies the newline characters. * * @param cmd * The complete command line call to use. * @param post_options * An associative array to json-encode and pass to the remote script on stdin. * @param backend_options * Options for the invocation. * * @return * If no commands were executed, FALSE. * * If one command was executed, this will return an associative array containing * the data from drush_backend_output(). The result code is stored * in $result['error_status'] (0 == no error). * * If multiple commands were executed, this will return an associative array * containing one item, 'concurrent', which will contain a list of the different * backend invoke results from each concurrent command. */ function _drush_backend_invoke($cmds, $common_backend_options = array(), $context = NULL) { if (drush_get_context('DRUSH_SIMULATE') && !array_key_exists('override-simulated', $common_backend_options) && !array_key_exists('backend-simulate', $common_backend_options)) { foreach ($cmds as $cmd) { drush_print(dt('Simulating backend invoke: !cmd', array('!cmd' => $cmd['cmd']))); } return FALSE; } foreach ($cmds as $cmd) { drush_log(dt('Backend invoke: !cmd', array('!cmd' => $cmd['cmd'])), 'command'); } if (!empty($common_backend_options['interactive']) || !empty($common_backend_options['fork'])) { foreach ($cmds as $cmd) { $exec_cmd = $cmd['cmd']; if (array_key_exists('fork', $common_backend_options)) { $exec_cmd .= ' --quiet &'; } $result_code = drush_shell_proc_open($exec_cmd); $ret = array('error_status' => $result_code); } } else { $process_limit = drush_get_option_override($common_backend_options, 'concurrency', 1); $procs = _drush_backend_proc_open($cmds, $process_limit, $context); $procs = is_array($procs) ? $procs : array($procs); $ret = array(); foreach ($procs as $site => $proc) { if (($proc['code'] == DRUSH_APPLICATION_ERROR) && isset($common_backend_options['integrate'])) { drush_set_error('DRUSH_APPLICATION_ERROR', dt("The external command could not be executed due to an application error.")); } if ($proc['output']) { $values = drush_backend_parse_output($proc['output'], $proc['backend-options'], $proc['outputted']); if (is_array($values)) { $values['site'] = $site; if (empty($ret)) { $ret = $values; } elseif (!array_key_exists('concurrent', $ret)) { $ret = array('concurrent' => array($ret, $values)); } else { $ret['concurrent'][] = $values; } } else { $ret = drush_set_error('DRUSH_FRAMEWORK_ERROR', dt("The command could not be executed successfully (returned: !return, code: !code)", array("!return" => $proc['output'], "!code" => $proc['code']))); } } } } return empty($ret) ? FALSE : $ret; } /** * Helper function that generates an anonymous site alias specification for * the given parameters. */ function drush_backend_generate_sitealias($backend_options) { // Ensure default values. $backend_options += array( 'remote-host' => NULL, 'remote-user' => NULL, 'ssh-options' => NULL, 'drush-script' => NULL, 'env-vars' => NULL ); return array( 'remote-host' => $backend_options['remote-host'], 'remote-user' => $backend_options['remote-user'], 'ssh-options' => $backend_options['ssh-options'], '#env-vars' => $backend_options['env-vars'], 'path-aliases' => array( '%drush-script' => $backend_options['drush-script'], ), ); } /** * Generate a command to execute. * * @param site_record * An array containing information used to generate the command. * 'remote-host' * Optional. A remote host to execute the drush command on. * 'remote-user' * Optional. Defaults to the current user. If you specify this, you can choose which module to send. * 'ssh-options' * Optional. Defaults to "-o PasswordAuthentication=no" * '#env-vars' * Optional. An associative array of environmental variables to prefix the Drush command with. * 'path-aliases' * Optional; contains paths to folders and executables useful to the command. * '%drush-script' * Optional. Defaults to the current drush.php file on the local machine, and * to simply 'drush' (the drush script in the current PATH) on remote servers. * You may also specify a different drush.php script explicitly. You will need * to set this when calling drush on a remote server if 'drush' is not in the * PATH on that machine. * @param command * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'. * @param args * An array of arguments for the command. * @param command_options * Optional. An array containing options to pass to the remote script. * Array items with a numeric key are treated as optional arguments to the * command. This parameter is a reference, as any options that have been * represented as either an option, or an argument will be removed. This * allows you to pass the left over options as a JSON encoded string, * without duplicating data. * @param backend_options * Optional. An array of options for the invocation. * @see drush_backend_invoke for documentation. * * @return * A text string representing a fully escaped command. */ function _drush_backend_generate_command($site_record, $command, $args = array(), $command_options = array(), $backend_options = array()) { $site_record += array( 'remote-host' => NULL, 'remote-user' => NULL, 'ssh-options' => NULL, 'path-aliases' => array(), ); $backend_options += array( '#tty' => FALSE, ); $hostname = $site_record['remote-host']; $username = $site_record['remote-user']; $ssh_options = $site_record['ssh-options']; $os = drush_os($site_record); if (drush_is_local_host($hostname)) { $hostname = null; } foreach ($command_options as $key => $arg) { if (is_numeric($key)) { $args[] = $arg; unset($command_options[$key]); } } $cmd[] = $command; foreach ($args as $arg) { $cmd[] = drush_escapeshellarg($arg, $os); } $option_str = _drush_backend_argument_string($command_options, $os); if (!empty($option_str)) { $cmd[] = " " . $option_str; } $command = implode(' ', array_filter($cmd, 'strlen')); if (isset($hostname)) { $username = (isset($username)) ? drush_escapeshellarg($username, "LOCAL") . "@" : ''; $ssh_options = $site_record['ssh-options']; $ssh_options = (isset($ssh_options)) ? $ssh_options : drush_get_option('ssh-options', "-o PasswordAuthentication=no"); $ssh_cmd[] = "ssh"; $ssh_cmd[] = $ssh_options; if ($backend_options['#tty']) { $ssh_cmd[] = '-t'; } $ssh_cmd[] = $username . drush_escapeshellarg($hostname, "LOCAL"); $ssh_cmd[] = drush_escapeshellarg($command . ' 2>&1', "LOCAL"); // Remove NULLs and separate with spaces $command = implode(' ', array_filter($ssh_cmd, 'strlen')); } return $command; } /** * Map the options to a string containing all the possible arguments and options. * * @param data * Optional. An array containing options to pass to the remote script. * Array items with a numeric key are treated as optional arguments to the command. * This parameter is a reference, as any options that have been represented as either an option, or an argument will be removed. * This allows you to pass the left over options as a JSON encoded string, without duplicating data. * @param method * Optional. Defaults to 'GET'. * If this parameter is set to 'POST', the $data array will be passed to the script being called as a JSON encoded string over * the STDIN pipe of that process. This is preferable if you have to pass sensitive data such as passwords and the like. * For any other value, the $data array will be collapsed down into a set of command line options to the script. * @return * A properly formatted and escaped set of arguments and options to append to the drush.php shell command. */ function _drush_backend_argument_string($data, $os = NULL) { $options = array(); foreach ($data as $key => $value) { if (!is_array($value) && !is_object($value) && isset($value)) { if (substr($key,0,1) != '#') { $options[$key] = $value; } } } $option_str = ''; foreach ($options as $key => $value) { $option_str .= _drush_escape_option($key, $value, $os); } return $option_str; } /** * Return a properly formatted and escaped command line option * * @param key * The name of the option. * @param value * The value of the option. * * @return * If the value is set to TRUE, this function will return " --key" * In other cases it will return " --key='value'" */ function _drush_escape_option($key, $value = TRUE, $os = NULL) { if ($value !== TRUE) { $option_str = " --$key=" . drush_escapeshellarg($value, $os); } else { $option_str = " --$key"; } return $option_str; } /** * Read options fron STDIN during POST requests. * * This function will read any text from the STDIN pipe, * and attempts to generate an associative array if valid * JSON was received. * * @return * An associative array of options, if successfull. Otherwise FALSE. */ function _drush_backend_get_stdin() { $fp = fopen('php://stdin', 'r'); // Windows workaround: we cannot count on stream_get_contents to // return if STDIN is reading from the keyboard. We will therefore // check to see if there are already characters waiting on the // stream (as there always should be, if this is a backend call), // and if there are not, then we will exit. // This code prevents drush from hanging forever when called with // --backend from the commandline; however, overall it is still // a futile effort, as it does not seem that backend invoke can // successfully write data to that this function can read, // so the argument list and command always come out empty. :( // Perhaps stream_get_contents is the problem, and we should use // the technique described here: // http://bugs.php.net/bug.php?id=30154 // n.b. the code in that issue passes '0' for the timeout in stream_select // in a loop, which is not recommended. // Note that the following DOES work: // drush ev 'print(json_encode(array("test" => "XYZZY")));' | drush status --backend // So, redirecting input is okay, it is just the proc_open that is a problem. if (drush_is_windows()) { // Note that stream_select uses reference parameters, so we need variables (can't pass a constant NULL) $read = array($fp); $write = NULL; $except = NULL; // Question: might we need to wait a bit for STDIN to be ready, // even if the process that called us immediately writes our parameters? // Passing '100' for the timeout here causes us to hang indefinitely // when called from the shell. $changed_streams = stream_select($read, $write, $except, 0); // Return on error (FALSE) or no changed streams (0). // Oh, according to http://php.net/manual/en/function.stream-select.php, // stream_select will return FALSE for streams returned by proc_open. // That is not applicable to us, is it? Our stream is connected to a stream // created by proc_open, but is not a stream returned by proc_open. if ($changed_streams < 1) { return FALSE; } } stream_set_blocking($fp, FALSE); $string = stream_get_contents($fp); fclose($fp); if (trim($string)) { return json_decode($string, TRUE); } return FALSE; } getCurrentUserAsSingle()->id(); drush_include_engine('drupal', 'batch'); _drush_backend_batch_process($command, $args, $options); } /** * Process sets from the specified batch. * * This function is called by the worker process that is spawned by the * drush_backend_batch_process function. * * The command called needs to call this function after it's special bootstrap * requirements have been taken care of. * * @param int $id * The batch ID of the batch being processed. */ function drush_batch_command($id) { include_once(DRUSH_DRUPAL_CORE . '/includes/batch.inc'); drush_include_engine('drupal', 'batch'); _drush_batch_command($id); } valid_root($path)) { return $candidate; } } return NULL; } /** * Check to see if there is a bootstrap class available * at the specified location; if there is, load it. */ function drush_load_bootstrap_commandfile_at_path($path) { static $paths = array(); if (!empty($path) && (!array_key_exists($path, $paths))) { $paths[$path] = TRUE; // Check to see if we have any bootstrap classes in this location. $bootstrap_class_dir = $path . '/drush/bootstrap'; if (is_dir($bootstrap_class_dir)) { _drush_add_commandfiles(array($bootstrap_class_dir), DRUSH_BOOTSTRAP_NONE); } } } /** * Select the bootstrap class to use. If this is called multiple * times, the bootstrap class returned might change on subsequent * calls, if the root directory changes. Once the bootstrap object * starts changing the state of the system, however, it will * be 'latched', and further calls to drush_select_bootstrap_class() * will always return the same object. */ function drush_select_bootstrap_class() { $root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); // Once we have selected a Drupal root, we will reduce our bootstrap // candidates down to just the one used to select this site root. $bootstrap = drush_bootstrap_class_for_root($root); // If we have not found a bootstrap class by this point, // then take the last one and use it. This should be our // default bootstrap class. The default bootstrap class // should pass through all calls without doing anything that // changes state in a CMS-specific way. if ($bootstrap == NULL) { $candidates = drush_get_bootstrap_candidates(); $bootstrap = array_pop($candidates); } return $bootstrap; } /** * Don't allow the bootstrap object to change once we start bootstrapping */ function drush_latch_bootstrap_object($bootstrap) { drush_set_context('DRUSH_BOOTSTRAP_OBJECT', $bootstrap); } /** * Get the appropriate bootstrap object. We'll search for a new * bootstrap object every time someone asks for one until we start * bootstrapping; then we'll returned the same cached one every time. * * @return \Drush\Boot\Boot */ function drush_get_bootstrap_object() { $bootstrap = drush_get_context('DRUSH_BOOTSTRAP_OBJECT', FALSE); if (!$bootstrap) { $bootstrap = drush_select_bootstrap_class(); } return $bootstrap; } /** * Find the URI that has been selected by the cwd * if it was not previously set via the --uri / -l option */ function _drush_bootstrap_selected_uri() { $uri = drush_get_context('DRUSH_SELECTED_URI'); if (empty($uri)) { $site_path = drush_site_path(); $elements = explode('/', $site_path); $current = array_pop($elements); if (!$current) { $current = 'default'; } $uri = 'http://'. $current; $uri = drush_set_context('DRUSH_SELECTED_URI', $uri); drush_sitealias_create_self_alias(); } return $uri; } /** * Helper function to store any context settings that are being validated. */ function drush_bootstrap_value($context, $value = null) { $values =& drush_get_context('DRUSH_BOOTSTRAP_VALUES', array()); if (isset($value)) { $values[$context] = $value; } if (array_key_exists($context, $values)) { return $values[$context]; } return null; } /** * Returns an array that determines what bootstrap phases * are necessary to bootstrap the CMS. * * @param bool $function_names * (optional) If TRUE, return an array of method names index by their * corresponding phase values. Otherwise return an array of phase values. * * @return array * * @see \Drush\Boot\Boot::bootstrap_phases() */ function _drush_bootstrap_phases($function_names = FALSE) { $result = array(); if ($bootstrap = drush_get_bootstrap_object()) { $result = $bootstrap->bootstrap_phases(); if (!$function_names) { $result = array_keys($result); } } return $result; } /** * Bootstrap Drush to the desired phase. * * This function will sequentially bootstrap each * lower phase up to the phase that has been requested. * * @param int $phase * The bootstrap phase to bootstrap to. * @param int $phase_max * (optional) The maximum level to boot to. This does not have a use in this * function itself but can be useful for other code called from within this * function, to know if e.g. a caller is in the process of booting to the * specified level. If specified, it should never be lower than $phase. * * @return bool * TRUE if the specified bootstrap phase has completed. * * @see \Drush\Boot\Boot::bootstrap_phases() */ function drush_bootstrap($phase, $phase_max = FALSE) { $bootstrap = drush_get_bootstrap_object(); $phases = _drush_bootstrap_phases(TRUE); $result = TRUE; // If the requested phase does not exist in the list of available // phases, it means that the command requires bootstrap to a certain // level, but no site root could be found. if (!isset($phases[$phase])) { $result = drush_bootstrap_error('DRUSH_NO_SITE', dt("We could not find an applicable site for that command.")); } // Once we start bootstrapping past the DRUSH_BOOTSTRAP_DRUSH phase, we // will latch the bootstrap object, and prevent it from changing. if ($phase > DRUSH_BOOTSTRAP_DRUSH) { drush_latch_bootstrap_object($bootstrap); } drush_set_context('DRUSH_BOOTSTRAPPING', TRUE); foreach ($phases as $phase_index => $current_phase) { $bootstrapped_phase = drush_get_context('DRUSH_BOOTSTRAP_PHASE', -1); if ($phase_index > $phase) { break; } if ($phase_index > $bootstrapped_phase) { if ($result = drush_bootstrap_validate($phase_index)) { if (method_exists($bootstrap, $current_phase) && !drush_get_error()) { drush_log(dt("Drush bootstrap phase : !function()", array('!function' => $current_phase)), LogLevel::BOOTSTRAP); $bootstrap->{$current_phase}(); // Reset commandfile cache and find any new command files that are available during this bootstrap phase. drush_get_commands(TRUE); _drush_find_commandfiles($phase_index, $phase_max); } drush_set_context('DRUSH_BOOTSTRAP_PHASE', $phase_index); } } } drush_set_context('DRUSH_BOOTSTRAPPING', FALSE); if (!$result || drush_get_error()) { $errors = drush_get_context('DRUSH_BOOTSTRAP_ERRORS', array()); foreach ($errors as $code => $message) { drush_set_error($code, $message); } } return !drush_get_error(); } /** * Determine whether a given bootstrap phase has been completed * * This function name has a typo which makes me laugh so we choose not to * fix it. Take a deep breath, and smile. See * http://en.wikipedia.org/wiki/HTTP_referer * * * @param int $phase * The bootstrap phase to test * * @return bool * TRUE if the specified bootstrap phase has completed. */ function drush_has_boostrapped($phase) { $phase_index = drush_get_context('DRUSH_BOOTSTRAP_PHASE'); return isset($phase_index) && ($phase_index >= $phase); } /** * Validate whether a bootstrap phase can be reached. * * This function will validate the settings that will be used * during the actual bootstrap process, and allow commands to * progressively bootstrap to the highest level that can be reached. * * This function will only run the validation function once, and * store the result from that execution in a local static. This avoids * validating phases multiple times. * * @param int $phase * The bootstrap phase to validate to. * * @return bool * TRUE if bootstrap is possible, FALSE if the validation failed. * * @see \Drush\Boot\Boot::bootstrap_phases() */ function drush_bootstrap_validate($phase) { $bootstrap = drush_get_bootstrap_object(); $phases = _drush_bootstrap_phases(TRUE); static $result_cache = array(); if (!array_key_exists($phase, $result_cache)) { drush_set_context('DRUSH_BOOTSTRAP_ERRORS', array()); drush_set_context('DRUSH_BOOTSTRAP_VALUES', array()); foreach ($phases as $phase_index => $current_phase) { $validated_phase = drush_get_context('DRUSH_BOOTSTRAP_VALIDATION_PHASE', -1); if ($phase_index > $phase) { break; } if ($phase_index > $validated_phase) { $current_phase .= '_validate'; if (method_exists($bootstrap, $current_phase)) { $result_cache[$phase_index] = $bootstrap->{$current_phase}(); } else { $result_cache[$phase_index] = TRUE; } drush_set_context('DRUSH_BOOTSTRAP_VALIDATION_PHASE', $phase_index); } } } return $result_cache[$phase]; } /** * Bootstrap to the specified phase. * * @param int $max_phase_index * Only attempt bootstrap to the specified level. * * @return bool * TRUE if the specified bootstrap phase has completed. */ function drush_bootstrap_to_phase($max_phase_index) { if ($max_phase_index == DRUSH_BOOTSTRAP_MAX) { // Bootstrap as far as we can without throwing an error, but log for // debugging purposes. drush_log(dt("Trying to bootstrap as far as we can."), 'debug'); drush_bootstrap_max(); return TRUE; } drush_log(dt("Bootstrap to phase !phase.", array('!phase' => $max_phase_index)), LogLevel::BOOTSTRAP); $phases = _drush_bootstrap_phases(); $result = TRUE; // Try to bootstrap to the maximum possible level, without generating errors foreach ($phases as $phase_index) { if ($phase_index > $max_phase_index) { // Stop trying, since we achieved what was specified. break; } if (drush_bootstrap_validate($phase_index)) { if ($phase_index > drush_get_context('DRUSH_BOOTSTRAP_PHASE', DRUSH_BOOTSTRAP_NONE)) { $result = drush_bootstrap($phase_index, $max_phase_index); } } else { $result = FALSE; break; } } return $result; } /** * Bootstrap to the highest level possible, without triggering any errors. * * @param int $max_phase_index * (optional) Only attempt bootstrap to the specified level. * * @return int * The maximum phase to which we bootstrapped. */ function drush_bootstrap_max($max_phase_index = FALSE) { $phases = _drush_bootstrap_phases(TRUE); if (!$max_phase_index) { $max_phase_index = count($phases); } // Try to bootstrap to the maximum possible level, without generating errors. foreach ($phases as $phase_index => $current_phase) { if ($phase_index > $max_phase_index) { // Stop trying, since we achieved what was specified. break; } if (drush_bootstrap_validate($phase_index)) { if ($phase_index > drush_get_context('DRUSH_BOOTSTRAP_PHASE')) { drush_bootstrap($phase_index, $max_phase_index); } } else { // drush_bootstrap_validate() only logs successful validations. For us, // knowing what failed can also be important. $previous = drush_get_context('DRUSH_BOOTSTRAP_PHASE'); drush_log(dt("Bootstrap phase !function() failed to validate; continuing at !current().", array('!function' => $current_phase, '!current' => $phases[$previous])), 'debug'); break; } } return drush_get_context('DRUSH_BOOTSTRAP_PHASE'); } /** * Bootstrap the specified site alias. The site alias must * be a valid alias to a local site. * * @param $site_record * The alias record for the given site alias. * @see drush_sitealias_get_record(). * @param $max_phase_index * Only attempt bootstrap to the specified level. * @returns TRUE if attempted to bootstrap, or FALSE * if no bootstrap attempt was made. */ function drush_bootstrap_max_to_sitealias($site_record, $max_phase_index = NULL) { if ((array_key_exists('root', $site_record) && !array_key_exists('remote-host', $site_record))) { drush_sitealias_set_alias_context($site_record); drush_bootstrap_max($max_phase_index); return TRUE; } return FALSE; } /** * Helper function to collect any errors that occur during the bootstrap process. * Always returns FALSE, for convenience. */ function drush_bootstrap_error($code, $message = null) { $errors = drush_get_context('DRUSH_BOOTSTRAP_ERRORS'); $errors[$code] = $message; drush_set_context('DRUSH_BOOTSTRAP_ERRORS', $errors); return FALSE; } function _drush_bootstrap_output_prepare() { // Note that as soon as we set the DRUSH_BACKEND context, we change // the behavior of drush_log(). It is therefore important that we // should not set this context until immediately before we call ob_start // (i.e., in this function). $backend = drush_set_context('DRUSH_BACKEND', drush_get_option('backend')); $quiet = drush_get_context('DRUSH_QUIET'); if ($backend) { // Load options passed as a JSON encoded string through STDIN. $stdin_options = _drush_backend_get_stdin(); if (is_array($stdin_options)) { drush_set_context('stdin', $stdin_options); } // Add an output buffer handler to collect output/pass through backend // packets. Using a chunksize of 2 ensures that each line is flushed // straight away. if ($quiet) { // Pass through of backend packets, discard regular output. ob_start('drush_backend_output_discard', 2); } else { // Collect output. ob_start('drush_backend_output_collect', 2); } } // In non-backend quiet mode we start buffering and discards it on command // completion. if ($quiet && !$backend) { ob_start(); } } /** * Used by a Drush extension to request that its Composer autoload * files be loaded by Drush, if they have not already been. * * Usage: * * function mycommandfile_drush_init() { * drush_autoload(__FILE__) * } * */ function drush_autoload($commandfile) { $already_added = commandfiles_cache()->add($commandfile); $dir = dirname($commandfile); $candidates = array("vendor/autoload.php", "../../../vendor/autoload.php"); $drush_autoload_file = drush_get_context('DRUSH_VENDOR_PATH', ''); foreach ($candidates as $candidate) { $autoload = $dir . '/' . $candidate; if (file_exists($autoload) && (realpath($autoload) != $drush_autoload_file)) { include $autoload; } } } get($cid); $mess = $ret ? "HIT" : "MISS"; drush_log(dt("Cache !mess cid: !cid", array('!mess' => $mess, '!cid' => $cid)), LogLevel::DEBUG); return $ret; } /** * Return data from the persistent cache when given an array of cache IDs. * * @param array $cids * An array of cache IDs for the data to retrieve. This is passed by * reference, and will have the IDs successfully returned from cache removed. * @param string $bin * The cache bin where the data is stored. * * @return * An array of the items successfully returned from cache indexed by cid. */ function drush_cache_get_multiple(array &$cids, $bin = 'default') { return _drush_cache_get_object($bin)->getMultiple($cids); } /** * Store data in the persistent cache. * * @param string $cid * The cache ID of the data to store. * * @param $data * The data to store in the cache. * @param string $bin * The cache bin to store the data in. * @param $expire * One of the following values: * - DRUSH_CACHE_PERMANENT: Indicates that the item should never be removed * unless explicitly told to using drush_cache_clear_all() with a cache ID. * - DRUSH_CACHE_TEMPORARY: Indicates that the item should be removed at * the next general cache wipe. * - A Unix timestamp: Indicates that the item should be kept at least until * the given time, after which it behaves like DRUSH_CACHE_TEMPORARY. * * @return bool */ function drush_cache_set($cid, $data, $bin = 'default', $expire = DRUSH_CACHE_PERMANENT) { $ret = _drush_cache_get_object($bin)->set($cid, $data, $expire); if ($ret) drush_log(dt("Cache SET cid: !cid", array('!cid' => $cid)), LogLevel::DEBUG); return $ret; } /** * Expire data from the cache. * * If called without arguments, expirable entries will be cleared from all known * cache bins. * * @param string $cid * If set, the cache ID to delete. Otherwise, all cache entries that can * expire are deleted. * @param string $bin * If set, the bin $bin to delete from. Mandatory * argument if $cid is set. * @param bool $wildcard * If $wildcard is TRUE, cache IDs starting with $cid are deleted in * addition to the exact cache ID specified by $cid. If $wildcard is * TRUE and $cid is '*' then the entire bin $bin is emptied. */ function drush_cache_clear_all($cid = NULL, $bin = 'default', $wildcard = FALSE) { if (!isset($cid) && !isset($bin)) { foreach (drush_cache_get_bins() as $bin) { _drush_cache_get_object($bin)->clear(); } return; } return _drush_cache_get_object($bin)->clear($cid, $wildcard); } /** * Check if a cache bin is empty. * * A cache bin is considered empty if it does not contain any valid data for any * cache ID. * * @param $bin * The cache bin to check. * * @return * TRUE if the cache bin specified is empty. */ function _drush_cache_is_empty($bin) { return _drush_cache_get_object($bin)->isEmpty(); } /** * Return drush cache bins and any bins added by hook_drush_flush_caches(). */ function drush_cache_get_bins() { $drush = array('default'); return array_merge(drush_command_invoke_all('drush_flush_caches'), $drush); } /** * Create a cache id from a given prefix, contexts, and additional parameters. * * @param prefix * A human readable cid prefix that will not be hashed. * @param contexts * Array of drush contexts that will be used to build a unique hash. * @param params * Array of any addition parameters to be hashed. * * @return * A cache id string. */ function drush_get_cid($prefix, $contexts = array(), $params = array()) { $cid = array(); foreach ($contexts as $context) { $c = drush_get_context($context); if (!empty($c)) { $cid[] = is_scalar($c) ? $c : serialize($c); } } foreach ($params as $param) { $cid[] = $param; } return DRUSH_VERSION . '-' . $prefix . '-' . md5(implode("", $cid)); } $command))); } } /** * Invoke a command in a new process, targeting the site specified by * the provided site alias record. * * @param array $site_alias_record * The site record to execute the command on. Use '@self' to run on the current site. * @param string $command_name * The command to invoke. * @param array $commandline_args * The arguments to pass to the command. * @param array $commandline_options * The options (e.g. --select) to provide to the command. * @param mixed $backend_options * TRUE - integrate errors * FALSE - do not integrate errors * array - @see drush_backend_invoke_concurrent * There are also several options that _only_ work when set in * this parameter. They include: * 'invoke-multiple' * If $site_alias_record represents a single site, then 'invoke-multiple' * will cause the _same_ command with the _same_ arguments and options * to be invoked concurrently (e.g. for running concurrent batch processes). * 'concurrency' * Limits the number of concurrent processes that will run at the same time. * Defaults to '4'. * 'override-simulated' * Forces the command to run, even in 'simulated' mode. Useful for * commands that do not change any state on the machine, e.g. to fetch * database information for sql-sync via sql-conf. * 'interactive' * Overrides the backend invoke process to run commands interactively. * 'fork' * Overrides the backend invoke process to run non blocking commands in * the background. Forks a new process by adding a '&' at the end of the * command. The calling process does not receive any output from the child * process. The fork option is used to spawn a process that outlives its * parent. * * @return * If the command could not be completed successfully, FALSE. * If the command was completed, this will return an associative * array containing the results of the API call. * @see drush_backend_get_result() * * Do not change the signature of this function! drush_invoke_process * is one of the key Drush APIs. See http://drupal.org/node/1152908 */ function drush_invoke_process($site_alias_record, $command_name, $commandline_args = array(), $commandline_options = array(), $backend_options = TRUE) { if (is_array($site_alias_record) && array_key_exists('site-list', $site_alias_record)) { list($site_alias_records, $not_found) = drush_sitealias_resolve_sitespecs($site_alias_record['site-list']); if (!empty($not_found)) { drush_log(dt("Not found: @list", array("@list" => implode(', ', $not_found))), LogLevel::WARNING); return FALSE; } $site_alias_records = drush_sitealias_simplify_names($site_alias_records); foreach ($site_alias_records as $alias_name => $alias_record) { $invocations[] = array( 'site' => $alias_record, 'command' => $command_name, 'args' => $commandline_args, ); } } else { $invocations[] = array( 'site' => $site_alias_record, 'command' => $command_name, 'args' => $commandline_args); $invoke_multiple = drush_get_option_override($backend_options, 'invoke-multiple', 0); if ($invoke_multiple) { $invocations = array_fill(0, $invoke_multiple, $invocations[0]); } } return drush_backend_invoke_concurrent($invocations, $commandline_options, $backend_options); } /** * Given a command record, dispatch it as if it were * the original command. Executes in the currently * bootstrapped site using the current option contexts. * Note that drush_dispatch will not bootstrap any further than the * current command has already bootstrapped; therefore, you should only invoke * commands that have the same (or lower) bootstrap requirements. * * @param command * A full $command such as returned by drush_get_commands(), * or a string containing the name of the command record from * drush_get_commands() to call. * @param arguments * An array of argument values. * * @see drush_topic_docs_topic(). */ function drush_dispatch($command, $arguments = array()) { drush_set_command($command); $return = FALSE; if ($command) { // Add arguments, if this has not already been done. // (If the command was fetched from drush_parse_command, // then you cannot provide arguments to drush_dispatch.) if (empty($command['arguments'])) { _drush_prepare_command($command, $arguments); } // Merge in the options added by hooks. We need this // for validation, but this $command is just going to // get thrown away, so we'll have to do this again later. annotationcommand_adapter_add_hook_options($command); // Add command-specific options, if applicable. drush_command_default_options($command); // Test to see if any of the options in the 'cli' context // are not represented in the command structure. if ((_drush_verify_cli_options($command) === FALSE) || (_drush_verify_cli_arguments($command) === FALSE)) { return FALSE; } // Give command files an opportunity to alter the command record drush_command_invoke_all_ref('drush_command_alter', $command); // Include and validate command engines. if (drush_load_command_engines($command) === FALSE) { return FALSE; } // Do tilde expansion immediately prior to execution, // so that tildes are passed through unchanged for // remote commands and other redispatches. drush_preflight_tilde_expansion($command); // Get the arguments for this command. Add the options // on the end if this is that kind of command. $args = $command['arguments']; // Call the callback function of the active command. $return = call_user_func_array($command['callback'], $args); } // Add a final log entry, just so a timestamp appears. drush_log(dt('Command dispatch complete'), LogLevel::NOTICE); return $return; } /** * Entry point for commands into the drush_invoke() API * * If a command does not have a callback specified, this function will be called. * * This function will trigger $hook_drush_init, then if no errors occur, * it will call drush_invoke() with the command that was dispatch. * * If no errors have occured, it will run $hook_drush_exit. */ function drush_command() { $args = func_get_args(); $command = drush_get_command(); foreach (drush_command_implements("drush_init") as $name) { $func = $name . '_drush_init'; if (drush_get_option('show-invoke')) { drush_log(dt("Calling global init hook: !func", array('!name' => $name, '!func' => $func . '()')), LogLevel::BOOTSTRAP); } call_user_func_array($func, $args); _drush_log_drupal_messages(); } if (!drush_get_error()) { $result = _drush_invoke_hooks($command, $args); } if (!drush_get_error()) { foreach (drush_command_implements('drush_exit') as $name) { $func = $name . '_drush_exit'; if (drush_get_option('show-invoke')) { drush_log(dt("Calling global exit hook: !func", array('!name' => $name, '!func' => $func . '()')), LogLevel::BOOTSTRAP); } call_user_func_array($func, $args); _drush_log_drupal_messages(); } } } /** * Invoke Drush API calls, including all hooks. * * This is an internal function; it is called from drush_dispatch via * drush_command, but only if the command does not specify a 'callback' * function. If a callback function is specified, it will be called * instead of drush_command + _drush_invoke_hooks. * * Executes the specified command with the specified arguments on the * currently bootstrapped site using the current option contexts. * Note that _drush_invoke_hooks will not bootstrap any further than the * current command has already bootstrapped; therefore, you should only invoke * commands that have the same (or lower) bootstrap requirements. * * Call the correct hook for all the modules that implement it. * Additionally, the ability to rollback when an error has been encountered is also provided. * If at any point during execution, the drush_get_error() function returns anything but 0, * drush_invoke() will trigger $hook_rollback for each of the hooks that implement it, * in reverse order from how they were executed. Rollbacks are also triggered any * time a hook function returns FALSE. * * This function will also trigger pre_$hook and post_$hook variants of the hook * and its rollbacks automatically. * * HOW DRUSH HOOK FUNCTIONS ARE NAMED: * * The name of the hook is composed from the name of the command and the name of * the command file that the command definition is declared in. The general * form for the hook filename is: * * drush_COMMANDFILE_COMMANDNAME * * In many cases, drush commands that are functionally part of a common collection * of similar commands will all be declared in the same file, and every command * defined in that file will start with the same command prefix. For example, the * command file "pm.drush.inc" defines commands such as "pm-enable" and "pm-disable". * In the case of "pm-enable", the command file is "pm", and and command name is * "pm-enable". When the command name starts with the same sequence of characters * as the command file, then the repeated sequence is dropped; thus, the command * hook for "pm-enable" is "drush_pm_enable", not "drush_pm_pm_enable". * * There is also a special Drupal-version-specific naming convention that may * be used. To hide a commandfile from all versions of Drupal except for the * specific one named, add a ".dVERSION" after the command prefix. For example, * the file "views.d8.drush.inc" defines a "views" commandfile that will only * load with Drupal 8. This feature is not necessary and should not be used * in contrib modules (any extension with a ".module" file), since these modules * are already version-specific. * * @param command * The drush command to execute. * @param args * An array of arguments to the command OR a single non-array argument. * @return * The return value will be passed along to the caller if --backend option is * present. A boolean FALSE indicates failure and rollback will be intitated. * * This function should not be called directly. * @see drush_invoke() and @see drush_invoke_process() */ function _drush_invoke_hooks($command, $args) { $return = null; // If someone passed a standalone arg, convert it to a single-element array if (!is_array($args)) { $args = array($args); } // Include the external command file used by this command, if there is one. drush_command_include($command['command-hook']); // Generate the base name for the hook by converting all // dashes in the command name to underscores. $hook = str_replace("-", "_", $command['command-hook']); // Call the hook init function, if it exists. // If a command needs to bootstrap, it is advisable // to do so in _init; otherwise, new commandfiles // will miss out on participating in any stage that // has passed or started at the time it was discovered. $func = 'drush_' . $hook . '_init'; if (function_exists($func)) { drush_log(dt("Calling drush command init function: !func", array('!func' => $func)), LogLevel::BOOTSTRAP); call_user_func_array($func, $args); _drush_log_drupal_messages(); if (drush_get_error()) { drush_log(dt('The command @command could not be initialized.', array('@command' => $command['command-hook'])), LogLevel::ERROR); return FALSE; } } // We will adapt and call as many of the annotated command hooks as we can. // The following command hooks are not supported in Drush 8.x: // - Command Event: not called (requires CommandEvent object) // - Option: Equivalent functionality supported in annotationcommand_adapter.inc // - Interact: not called (We don't use SymfonyStyle in 8.x at the moment) // - Status: not called - probably not needed? // - Extract not called - probably not needed? // The hooks that are called include: // - Pre-initialize, initialize and post-initialize // - Pre-validate and validate // - Pre-command, command and post-command // - Pre-process, process and post-process // - Pre-alter, alter and post-alter $names = annotationcommand_adapter_command_names($command); // Merge in the options added by hooks (again) annotationcommand_adapter_add_hook_options($command); $annotationData = $command['annotations']; $input = new DrushInputAdapter($args, annotationcommand_adapter_get_options($command), $command['command']); $output = new DrushOutputAdapter(); $commandData = new CommandData( $annotationData, $input, $output, false, false ); annotationcommand_adapter_call_initialize($names, $commandData); $rollback = FALSE; $completed = array(); $available_rollbacks = array(); $all_available_hooks = array(); // Iterate through the different hook variations $variations = array( 'pre_validate' => $hook . "_pre_validate", 'validate' => $hook . "_validate", 'pre_command' => "pre_$hook", 'command' => $hook, 'post_command' => "post_$hook" ); foreach ($variations as $hook_phase => $var_hook) { $adapterHookFunction = 'annotationcommand_adapter_call_hook_' . $hook_phase; $adapterHookFunction($names, $commandData, $return); // Get the list of command files. // We re-fetch the list every time through // the loop in case one of the hook function // does something that will add additional // commandfiles to the list (i.e. bootstrapping // to a higher phase will do this). $list = drush_commandfile_list(); // Make a list of function callbacks to call. If // there is a 'primary function' mentioned, make sure // that it appears first in the list, but only if // we are running the main hook ("$hook"). After that, // make sure that any callback associated with this commandfile // executes before any other hooks defined in any other // commandfiles. $callback_list = array(); if (($var_hook == $hook) && ($command['primary function'])) { $callback_list[$command['primary function']] = $list[$command['commandfile']]; } else { $primary_func = ($command['commandfile'] . "_" == substr($var_hook . "_",0,strlen($command['commandfile']) + 1)) ? sprintf("drush_%s", $var_hook) : sprintf("drush_%s_%s", $command['commandfile'], $var_hook); $callback_list[$primary_func] = $list[$command['commandfile']]; } // We've got the callback for the primary function in the // callback list; now add all of the other callback functions. unset($list[$command['commandfile']]); foreach ($list as $commandfile => $filename) { $func = sprintf("drush_%s_%s", $commandfile, $var_hook); $callback_list[$func] = $filename; } // Run all of the functions available for this variation $accumulated_result = NULL; foreach ($callback_list as $func => $filename) { if (function_exists($func)) { $all_available_hooks[] = $func . ' [* Defined in ' . $filename . ']'; $available_rollbacks[] = $func . '_rollback'; $completed[] = $func; drush_log(dt("Calling hook !hook", array('!hook' => $func)), LogLevel::DEBUG); try { $result = call_user_func_array($func, $args); drush_log(dt("Returned from hook !hook", array('!hook' => $func)), LogLevel::DEBUG); } catch (Exception $e) { drush_set_error('DRUSH_EXECUTION_EXCEPTION', (string) $e); } // If there is an error, break out of the foreach // $variations and foreach $callback_list if (drush_get_error() || ($result === FALSE)) { $rollback = TRUE; break 2; } // If result values are arrays, then combine them all together. // Later results overwrite earlier results. if (isset($result) && is_array($accumulated_result) && is_array($result)) { $accumulated_result = array_merge($accumulated_result, $result); } else { $accumulated_result = $result; } _drush_log_drupal_messages(); } else { $all_available_hooks[] = $func; } } // Process the result value from the 'main' callback hook only. if ($var_hook == $hook) { $return = $accumulated_result; if (isset($return)) { annotationcommand_adapter_call_hook_process_and_alter($names, $commandData, $return); drush_handle_command_output($command, $return); } } } // If no hook functions were found, print a warning. if (empty($completed)) { $default_command_hook = sprintf("drush_%s_%s", $command['commandfile'], $hook); if (($command['commandfile'] . "_" == substr($hook . "_",0,strlen($command['commandfile'])+ 1))) { $default_command_hook = sprintf("drush_%s", $hook); } $dt_args = array( '!command' => $command['command-hook'], '!default_func' => $default_command_hook, ); $message = "No hook functions were found for !command. The primary hook function is !default_func(). Please implement this function. Run with --show-invoke to see all available hooks."; $return = drush_set_error('DRUSH_FUNCTION_NOT_FOUND', dt($message, $dt_args)); } if (drush_get_option('show-invoke')) { // We show all available hooks up to and including the one that failed (or all, if there were no failures) drush_log(dt("Available drush_invoke() hooks for !command: !available", array('!command' => $command['command-hook'], '!available' => "\n" . implode("\n", $all_available_hooks))), LogLevel::OK); } if (drush_get_option('show-invoke') && !empty($available_rollbacks)) { drush_log(dt("Available rollback hooks for !command: !rollback", array('!command' => $command['command-hook'], '!rollback' => "\n" . implode("\n", $available_rollbacks))), LogLevel::OK); } // Something went wrong, we need to undo. if ($rollback) { if (drush_get_option('confirm-rollback', FALSE)) { // Optionally ask for confirmation, --yes and --no are ignored from here on as we are about to finish this process. drush_set_context('DRUSH_AFFIRMATIVE', FALSE); drush_set_context('DRUSH_NEGATIVE', FALSE); $rollback = drush_confirm(dt('Do you want to rollback? (manual cleanup might be required otherwise)')); } if ($rollback) { foreach (array_reverse($completed) as $func) { $rb_func = $func . '_rollback'; if (function_exists($rb_func)) { call_user_func_array($rb_func, $args); _drush_log_drupal_messages(); drush_log(dt("Changes made in !func have been rolled back.", array('!func' => $func)), LogLevel::DEBUG); } } } $return = FALSE; } if (isset($return)) { return $return; } } /** * Convert the structured output array provided from the Drush * command into formatted output. Output is only printed for commands * that define 'default-format' &/or 'default-pipe-format'; all * other commands are expected to do their own output. */ function drush_handle_command_output($command, $structured_output) { // If the hook already called drush_backend_set_result, // then return that value. If it did not, then the return // value from the hook will be the value returned from // this routine. $return = drush_backend_get_result(); if (empty($return)) { drush_backend_set_result($structured_output); } // We skip empty strings and empty arrays, but note that 'empty' // returns TRUE for the integer value '0', but we do want to print that. // Only handle output here if the command defined an output format // engine. If no engine was declared, then we presume that the command // handled its own output. if ((!empty($structured_output) || ($structured_output === 0))) { // If the command specifies a default pipe format and // returned a result, then output the formatted output when // in --pipe mode. $formatter = drush_get_outputformat(); if (!$formatter && is_string($structured_output)) { $formatter = drush_load_engine('outputformat', 'string'); } if ($formatter) { if ($formatter === TRUE) { return drush_set_error(dt('No outputformat class defined for !format', array('!format' => $format))); } if ((!empty($command['engines']['outputformat'])) && (!in_array($formatter->engine, $command['engines']['outputformat']['usable']))) { return $formatter->format_error(dt("The command '!command' does not produce output in a structure usable by this output format.", array('!command' => $command['command']))); } // Add any user-specified options to the metadata passed to the formatter. $metadata = array(); $metadata['strict'] = drush_get_option('strict', FALSE); if (isset($formatter->engine_config['options'])) { $machine_parsable = $formatter->engine_config['engine-info']['machine-parsable']; if (drush_get_option('full', FALSE)) { if (isset($formatter->engine_config['fields-full'])) { $formatter->engine_config['fields-default'] = $formatter->engine_config['fields-full']; } else { $formatter->engine_config['fields-default'] = array_keys($formatter->engine_config['field-labels']); } } elseif ((drush_get_context('DRUSH_PIPE') || $machine_parsable) && isset($formatter->engine_config['fields-pipe'])) { $formatter->engine_config['fields-default'] = $formatter->engine_config['fields-pipe']; } // Determine the --format, and options relevant for that format. foreach ($formatter->engine_config['options'] as $option => $option_info) { $default_value = isset($formatter->engine_config[$option . '-default']) ? $formatter->engine_config[$option . '-default'] : FALSE; if (($default_value === FALSE) && array_key_exists('default', $option_info)) { $default_value = $option_info['default']; } if (isset($option_info['list'])) { $user_specified_value = drush_get_option_list($option, $default_value); } else { $user_specified_value = drush_get_option($option, $default_value); } if ($user_specified_value !== FALSE) { if (array_key_exists('key', $option_info)) { $option = $option_info['key']; } $metadata[$option] =$user_specified_value; } } } if (isset($metadata['fields']) && !empty($metadata['fields'])) { if (isset($formatter->engine_config['field-labels'])) { $formatter->engine_config['field-labels'] = drush_select_fields($formatter->engine_config['field-labels'], $metadata['fields'], $metadata['strict']); } } $output = $formatter->process($structured_output, $metadata); if (drush_get_context('DRUSH_PIPE')) { drush_print_pipe($output); } else { drush_print($output); } } } } /** * Fail with an error if the user specified options on the * command line that are not documented in the current command * record. Also verify that required options are present. */ function _drush_verify_cli_options($command) { // Start out with just the options in the current command record. $options = _drush_get_command_options($command); // Skip all tests if the command is marked to allow anything. // Also skip backend commands, which may have options on the commandline // that were inherited from the calling command. if (($command['allow-additional-options'] === TRUE)) { return TRUE; } // If 'allow-additional-options' contains a list of command names, // then union together all of the options from all of the commands. if (is_array($command['allow-additional-options'])) { $implemented = drush_get_commands(); foreach ($command['allow-additional-options'] as $subcommand_name) { if (array_key_exists($subcommand_name, $implemented)) { $options = array_merge($options, _drush_get_command_options($implemented[$subcommand_name])); } } } // Also add in global options $options = array_merge($options, drush_get_global_options()); // Add a placeholder option so that backend requests originating from prior versions of Drush are valid. $options += array('invoke' => ''); // Now we will figure out which options in the cli context // are not represented in our options list. $cli_options = array_keys(drush_get_context('cli')); $allowed_options = _drush_flatten_options($options); $allowed_options = drush_append_negation_options($allowed_options); $disallowed_options = array_diff($cli_options, $allowed_options); if (!empty($disallowed_options)) { $unknown = count($disallowed_options) > 1 ? dt('Unknown options') : dt('Unknown option'); if (drush_get_option('strict', TRUE)) { $msg = dt("@unknown: --@options. See `drush help @command` for available options. To suppress this error, add the option --strict=0.", array('@unknown' => $unknown, '@options' => implode(', --', $disallowed_options), '@command' => $command['command'])); return drush_set_error('DRUSH_UNKNOWN_OPTION', $msg); } } // Next check to see if all required options were specified, // and if all specified options with required values have values. $missing_required_options = array(); $options_missing_required_values = array(); foreach ($command['options'] as $option_name => $option) { if (is_array($option) && !empty($option['required']) && drush_get_option($option_name, NULL) === NULL) { $missing_required_options[] = $option_name; } // Note that drush_get_option() will return TRUE if an option // was specified without a value (--option), as opposed to // the string "1" is --option=1 was used. elseif (is_array($option) && !empty($option['value']) && ($option['value'] == 'required') && drush_get_option($option_name, NULL) === TRUE) { $options_missing_required_values[] = $option_name; } } if (!empty($missing_required_options) || !empty($options_missing_required_values)) { $missing_message = ''; if (!empty($missing_required_options)) { $missing = count($missing_required_options) > 1 ? dt('Missing required options') : dt('Missing required option'); $missing_message = dt("@missing: --@options.", array('@missing' => $missing, '@options' => implode(', --', $missing_required_options))); } if (!empty($options_missing_required_values)) { if (!empty($missing_message)) { $missing_message .= " "; } $missing = count($options_missing_required_values) > 1 ? dt('Options used without providing required values') : dt('Option used without a value where one was required'); $missing_message .= dt("@missing: --@options.", array('@missing' => $missing, '@options' => implode(', --', $options_missing_required_values))); } return drush_set_error(dt("!message See `drush help @command` for information on usage.", array('!message' => $missing_message, '@command' => $command['command']))); } return TRUE; } function drush_append_negation_options($allowed_options) { $new_allowed = $allowed_options; foreach ($allowed_options as $option) { $new_allowed[] = 'no-' . $option; } return $new_allowed; } function _drush_verify_cli_arguments($command) { // Check to see if all of the required arguments // are specified. if ($command['required-arguments']) { $required_arg_count = $command['required-arguments']; if ($required_arg_count === TRUE) { $required_arg_count = count($command['argument-description']); } if (count($command['arguments']) < $required_arg_count) { $missing = $required_arg_count > 1 ? dt('Missing required arguments') : dt('Missing required argument'); $required = array_slice(array_keys($command['argument-description']), 0, $required_arg_count); return drush_set_error(dt("@missing: @required. See `drush help @command` for information on usage.", array( '@missing' => $missing, '@required' => implode(", ", $required), '@command' => $command['command'], ))); } } return TRUE; } /** * Return the list of all of the options for the given * command record by merging the 'options' and 'sub-options' * records. */ function _drush_get_command_options($command) { drush_command_invoke_all_ref('drush_help_alter', $command); $options = $command['options']; foreach ($command['sub-options'] as $group => $suboptions) { $options = array_merge($options, $suboptions); } return $options; } /** * Return the list of all of the options for the given * command record including options provided by engines and additional-options. */ function drush_get_command_options_extended($command) { drush_merge_engine_data($command); // Start out with just the options in the current command record. $options = _drush_get_command_options($command); // If 'allow-additional-options' contains a list of command names, // then union together all of the options from all of the commands. if (is_array($command['allow-additional-options'])) { $implemented = drush_get_commands(); foreach ($command['allow-additional-options'] as $subcommand_name) { if (array_key_exists($subcommand_name, $implemented)) { $options = array_merge($options, _drush_get_command_options($implemented[$subcommand_name])); } } } return $options; } /** * Return the array keys of $options, plus any 'short-form' * representations that may appear in the option's value. */ function _drush_flatten_options($options) { $flattened_options = array(); foreach($options as $key => $value) { // engine sections start with 'package-handler=git_drupalorg', // or something similar. Get rid of everything from the = onward. if (($eq_pos = strpos($key, '=')) !== FALSE) { $key = substr($key, 0, $eq_pos); } $flattened_options[] = $key; if (is_array($value)) { if (array_key_exists('short-form', $value)) { $flattened_options[] = $value['short-form']; } } } return $flattened_options; } /** * Get the options that were passed to the current command. * * This function returns an array that contains all of the options * that are appropriate for forwarding along to drush_invoke_process. * * @return * An associative array of option key => value pairs. */ function drush_redispatch_get_options() { $options = array(); // Add in command-specific and alias options, but for global options only. $options_soup = drush_get_context('specific') + drush_get_context('alias'); $global_option_list = drush_get_global_options(FALSE); foreach ($options_soup as $key => $value) { if (array_key_exists($key, $global_option_list)) { $options[$key] = $value; } } // Local php settings should not override sitealias settings. $cli_context = drush_get_context('cli'); unset($cli_context['php'], $cli_context['php-options']); // Pass along CLI parameters, as higher priority. $options = $cli_context + $options; $options = array_diff_key($options, array_flip(drush_sitealias_site_selection_keys())); unset($options['command-specific']); unset($options['path-aliases']); // If we can parse the current command, then examine all contexts // in order for any option that is directly related to the current command $command = drush_parse_command(); if (is_array($command)) { foreach (drush_get_command_options_extended($command) as $key => $value) { $value = drush_get_option($key); if (isset($value)) { $options[$key] = $value; } } } // If --bootstrap-to-first-arg is specified, do not // pass it along to remote commands. unset($options['bootstrap-to-first-arg']); return $options; } /** * @} End of "defgroup dispatching". */ /** * @file * The drush command engine. * * Since drush can be invoked independently of a proper Drupal * installation and commands may operate across sites, a distinct * command engine is needed. * * It mimics the Drupal module engine in order to economize on * concepts and to make developing commands as familiar as possible * to traditional Drupal module developers. */ /** * Parse console arguments. */ function drush_parse_args() { $args = drush_get_context('argv'); $command_args = NULL; $global_options = array(); $target_alias_name = NULL; // It would be nice if commandfiles could somehow extend this list, // but it is not possible. We need to parse args before we find commandfiles, // because the specified options may affect how commandfiles are located. // Therefore, commandfiles are loaded too late to affect arg parsing. // There are only a limited number of short options anyway; drush reserves // all for use by drush core. static $arg_opts = array('c', 'u', 'r', 'l', 'i'); // Check to see if we were executed via a "#!/usr/bin/env drush" script drush_adjust_args_if_shebang_script($args); // Now process the command line arguments. We will divide them // into options (starting with a '-') and arguments. $arguments = $options = array(); for ($i = 1; $i < count($args); $i++) { $opt = $args[$i]; // We set $command_args to NULL until the first argument that is not // an alias is found (the command); we put everything that follows // into $command_args. if (is_array($command_args)) { $command_args[] = $opt; } // Is the arg an option (starting with '-')? if (!empty($opt) && $opt{0} == "-" && strlen($opt) != 1) { // Do we have multiple options behind one '-'? if (strlen($opt) > 2 && $opt{1} != "-") { // Each char becomes a key of its own. for ($j = 1; $j < strlen($opt); $j++) { $options[substr($opt, $j, 1)] = TRUE; } } // Do we have a longopt (starting with '--')? elseif ($opt{1} == "-") { if ($pos = strpos($opt, '=')) { $options[substr($opt, 2, $pos - 2)] = substr($opt, $pos + 1); } else { $options[substr($opt, 2)] = TRUE; } } else { $opt = substr($opt, 1); // Check if the current opt is in $arg_opts (= has to be followed by an argument). if ((in_array($opt, $arg_opts))) { // Raising errors for missing option values should be handled by the // bootstrap or specific command, so we no longer do this here. $options[$opt] = $args[$i + 1]; $i++; } else { $options[$opt] = TRUE; } } } // If it's not an option, it's a command. else { $arguments[] = $opt; // Once we find the first argument, record the command args and global options if (!is_array($command_args)) { // Remember whether we set $target_alias_name on a previous iteration, // then record the $target_alias_name iff this arguement references a valid site alias. $already_set_target = is_string($target_alias_name); if (!$already_set_target && drush_sitealias_valid_alias_format($opt)) { $target_alias_name = $opt; } // If an alias record was set on a previous iteration, then this // argument must be the command name. If we set the target alias // record on this iteration, then this is not the command name. // If we've found the command name, then save $options in $global_options // (all options that came before the command name), and initialize // $command_args to an array so that we will begin storing all args // and options that follow the command name in $command_args. if ($already_set_target || (!is_string($target_alias_name))) { $command_args = array(); $global_options = $options; } } } } // If no arguments are specified, then the command will // be either 'help' or 'version' (the latter if --version is specified) // @todo: it would be handy if one could do `drush @remote st --help` and // have that show help for st. Today, that shows --help for help command! if (!count($arguments)) { if (array_key_exists('version', $options)) { $arguments = array('version'); } else { $arguments = array('help'); } } if (is_array($command_args)) { drush_set_context('DRUSH_COMMAND_ARGS', $command_args); } drush_set_context('DRUSH_GLOBAL_CLI_OPTIONS', $global_options); // Handle the "@shift" alias, if present drush_process_bootstrap_to_first_arg($arguments); drush_set_arguments($arguments); drush_set_config_special_contexts($options); drush_set_context('cli', $options); return $arguments; } /** * Pop an argument off of drush's argument list */ function drush_shift() { $arguments = drush_get_arguments(); $result = NULL; if (!empty($arguments)) { // The php-script command uses the DRUSH_SHIFT_SKIP // context to cause drush_shift to skip the 'php-script' // command and the script path argument when it is // called from the user script. $skip_count = drush_get_context('DRUSH_SHIFT_SKIP'); if (is_numeric($skip_count)) { for ($i = 0; $i < $skip_count; $i++) { array_shift($arguments); } $skip_count = drush_set_context('DRUSH_SHIFT_SKIP', 0); } $result = array_shift($arguments); drush_set_arguments($arguments); } return $result; } /** * Special checking for "shebang" script handling. * * If there is a file 'script.php' that begins like so: * #!/path/to/drush * Then $args will be: * /path/to/drush /path/to/script userArg1 userArg2 ... * If it instead starts like this: * #!/path/to/drush --flag php-script * Then $args will be: * /path/to/drush "--flag php-script" /path/to/script userArg1 userArg2 ... * (Note that execve does not split the parameters from * the shebang line on whitespace; see http://en.wikipedia.org/wiki/Shebang_%28Unix%29) * When drush is called via one of the "shebang" lines above, * the first or second parameter will be the full path * to the "shebang" script file -- and if the path to the * script is in the second position, then we will expect that * the argument in the first position must begin with a * '@' (alias) or '-' (flag). Under ordinary circumstances, * we do not expect that the drush command must come before * any argument that is the full path to a file. We use * this assumption to detect "shebang" script execution. */ function drush_adjust_args_if_shebang_script(&$args) { if (drush_has_bash()) { // The drush.launcher script may add --php or --php-options at the // head of the argument list; skip past those. $base_arg_number = 1; while (substr($args[$base_arg_number], 0, 5) == '--php') { ++$base_arg_number; } if (_drush_is_drush_shebang_script($args[$base_arg_number])) { // If $args[1] is a drush "shebang" script, we will insert // the option "--bootstrap-to-first-arg" and the command // "php-script" at the beginning of @args, so the command // line args become: // /path/to/drush --bootstrap-to-first-arg php-script /path/to/script userArg1 userArg2 ... drush_set_option('bootstrap-to-first-arg', TRUE); array_splice($args, $base_arg_number, 0, array('php-script')); drush_set_context('DRUSH_SHEBANG_SCRIPT', TRUE); } elseif (((strpos($args[$base_arg_number], ' ') !== FALSE) || (!ctype_alnum($args[$base_arg_number][0]))) && (_drush_is_drush_shebang_script($args[$base_arg_number + 1]))) { // If $args[2] is a drush "shebang" script, we will insert // the space-exploded $arg[1] in place of $arg[1], so the // command line args become: // /path/to/drush scriptArg1 scriptArg2 ... /path/to/script userArg1 userArg2 ... // If none of the script arguments look like a drush command, // then we will insert "php-script" as the default command to // execute. $script_args = explode(' ', $args[$base_arg_number]); $has_command = FALSE; foreach ($script_args as $script_arg) { if (preg_match("/^[a-z][a-z0-9-]*$/",$script_arg)) { $has_command = TRUE; } } if (!$has_command) { $script_args[] = 'php-script'; } array_splice($args, 1, $base_arg_number, $script_args); drush_set_context('DRUSH_SHEBANG_SCRIPT', TRUE); } } } /** * Process the --bootstrap-to-first-arg option, if it is present. * * This option checks to see if the first user-provided argument is an alias * or site specification; if it is, it will be shifted into the first argument * position, where it will specify the site to bootstrap. The result of this * is that if your shebang line looks like this: * * #!/path/to/drush --bootstrap-to-first-arg php-script * * Then when you run that script, you can optionally provide an alias such * as @dev as the first argument (e.g. $ ./mydrushscript.php @dev scriptarg1 * scriptarg2). Since this is the behavior that one would usually want, * it is default behavior for a canonical script. That is, a script * with a simple shebang line, like so: * * #!/path/to/drush * * will implicitly have "--bootstrap-to-first-arg" and "php-script" prepended, and will therefore * behave exactly like the first example. To write a script that does not * use --bootstrap-to-first-arg, then the drush command or at least one flag must be explicitly * included, like so: * * #!/path/to/drush php-script */ function drush_process_bootstrap_to_first_arg(&$arguments) { if (drush_get_option('bootstrap-to-first-arg', FALSE)) { $shift_alias_pos = 1 + (drush_get_context('DRUSH_SHEBANG_SCRIPT') === TRUE); if (count($arguments) >= $shift_alias_pos) { $shifted_alias = $arguments[$shift_alias_pos]; $alias_record = drush_sitealias_get_record($shifted_alias); if (!empty($alias_record)) { // Move the alias we shifted from its current position // in the argument list to the front of the list array_splice($arguments, $shift_alias_pos, 1); array_unshift($arguments, $shifted_alias); } } } } /** * Get a list of all implemented commands. * This invokes hook_drush_command(). * * @return * Associative array of currently active command descriptors. * */ function drush_get_commands($reset = FALSE) { static $commands = array(); if ($reset) { $commands = array(); return; } elseif ($commands) { return $commands; } $list = drush_commandfile_list(); foreach ($list as $commandfile => $path) { if (drush_command_hook($commandfile, 'drush_command')) { $function = $commandfile . '_drush_command'; $result = $function(); foreach ((array)$result as $key => $command) { // Add some defaults and normalize the command descriptor. $command += drush_command_defaults($key, $commandfile, $path); // Add engine data. drush_merge_engine_data($command); // Translate command. drush_command_translate($command); // If the command callback is not 'drush_command', then // copy the callback function to an alternate element // of the command array that will be called when Drush // calls the command function hooks. Then, set the // callback to drush_command so that the function hooks // will be called. if (($command['callback'] != 'drush_command') && $command['invoke hooks']) { $command['primary function'] = $command['callback']; $command['callback'] = 'drush_command'; } $commands[$key] = $command; } } } $commands = array_merge($commands, annotationcommand_adapter_commands()); foreach ($commands as $command) { // For every alias, make a copy of the command and store it in the command list // using the alias as a key if (isset($command['aliases']) && count($command['aliases'])) { foreach ($command['aliases'] as $alias) { $commands[$alias] = $command; $commands[$alias]['is_alias'] = TRUE; } } } return $commands; } /** * Organize commands into categories. Used by help listing and core-cli. * * @param array $commands * A commands array as per drush_get_commands(). * * @return array $command_categories * A categorized associative array of commands. */ function drush_commands_categorize($commands) { $command_categories = array(); $category_map = array(); foreach ($commands as $key => $candidate) { if ((!array_key_exists('is_alias', $candidate) || !$candidate['is_alias']) && !$candidate['hidden']) { $category = $candidate['category']; // If we have decided to remap a category, remap every command if (array_key_exists($category, $category_map)) { $category = $category_map[$category]; } if (!array_key_exists($category, $command_categories)) { $title = drush_command_invoke_all('drush_help', "meta:$category:title"); $alternate_title = ''; if (!$title) { // If there is no title, then check to see if the // command file is stored in a folder with the same // name as some other command file (e.g. 'core') that // defines a title. $alternate = basename($candidate['path']); $alternate_title = drush_command_invoke_all('drush_help', "meta:$alternate:title"); } if (!empty($alternate_title)) { $category_map[$category] = $alternate; $category = $alternate; $title = $alternate_title; } $command_categories[$category]['title'] = empty($title) ? '' : $title[0]; $summary = drush_command_invoke_all('drush_help', "meta:$category:summary"); if ($summary) { $command_categories[$category]['summary'] = $summary[0]; } } $candidate['category'] = $category; $command_categories[$category]['commands'][$key] = $candidate; } } // Make sure that 'core' is always first in the list $core_category = array('core' => $command_categories['core']); unset($command_categories['core']); // Post-process the categories that have no title. // Any that have fewer than 4 commands go into a section called "other". $processed_categories = array(); $misc_categories = array(); $other_commands = array(); $other_categories = array(); foreach ($command_categories as $key => $info) { if (empty($info['title'])) { $one_category = $key; if (count($info['commands']) < 4) { $other_commands = array_merge($other_commands, $info['commands']); $other_categories[] = $one_category; } else { $info['title'] = dt("All commands in !category", array('!category' => $key)); $misc_categories[$one_category] = $info; } } else { $processed_categories[$key] = $info; } } $other_category = array(); if (!empty($other_categories)) { $other_category[implode(',', $other_categories)] = array('title' => dt("Other commands"), 'commands' => $other_commands); } asort($processed_categories); asort($misc_categories); $command_categories = array_merge($core_category, $processed_categories, $misc_categories, $other_category); // If the user specified --sort, then merge all of the remaining // categories together if (drush_get_option('sort', FALSE)) { $combined_commands = array(); foreach ($command_categories as $key => $info) { $combined_commands = array_merge($combined_commands, $info['commands']); } $command_categories = array('all' => array('commands' => $combined_commands, 'title' => dt("Commands:"))); } return $command_categories; } function drush_command_defaults($key, $commandfile, $path) { $defaults = array( 'command' => $key, 'command-hook' => $key, 'invoke hooks' => TRUE, 'callback arguments' => array(), 'commandfile' => $commandfile, 'path' => dirname($path), 'engines' => array(), // Helpful for drush_show_help(). 'callback' => 'drush_command', 'primary function' => FALSE, 'description' => NULL, 'sections' => array( 'examples' => 'Examples', 'arguments' => 'Arguments', 'options' => 'Options', ), 'arguments' => array(), 'required-arguments' => FALSE, 'options' => array(), 'sub-options' => array(), 'allow-additional-options' => FALSE, 'global-options' => array(), 'examples' => array(), 'aliases' => array(), 'core' => array(), 'scope' => 'site', 'drush dependencies' => array(), 'handle-remote-commands' => FALSE, 'remote-tty' => FALSE, 'strict-option-handling' => FALSE, 'tilde-expansion' => TRUE, 'bootstrap_errors' => array(), 'topics' => array(), 'hidden' => FALSE, 'category' => $commandfile, 'add-options-to-arguments' => FALSE, 'consolidation-output-formatters' => FALSE, 'annotated-command-callback' => '', 'annotations' => new AnnotationData(['command' => $key]), ); // We end up here, setting the defaults for a command, when // called from drush_get_global_options(). Early in the Drush // bootstrap, there will be no bootstrap object, because we // need to get the list of global options when loading config // files, and config files are loaded before the bootstrap object // is created. In this early stage, we just use the core global // options list. Later, the bootstrap object can also provide // additional defaults if needed. The bootstrap command defaults // will be merged into the command object again just before // running it in bootstrap_and_dispatch(). if ($bootstrap = drush_get_bootstrap_object()) { $defaults = array_merge($defaults, $bootstrap->command_defaults()); } return $defaults; } /** * Translates description and other keys of a command definition. * * @param $command * A command definition. */ function drush_command_translate(&$command) { $command['description'] = _drush_command_translate($command['description']); $keys = array('arguments', 'options', 'examples', 'sections'); foreach ($keys as $key) { foreach ($command[$key] as $k => $v) { if (is_array($v)) { $v['description'] = _drush_command_translate($v['description']); } else { $v = _drush_command_translate($v); } $command[$key][$k] = $v; } } } /** * Helper function for drush_command_translate(). * * @param $source * String or array. */ function _drush_command_translate($source) { return is_array($source) ? call_user_func_array('dt', $source) : dt($source); } /** * Matches a commands array, as returned by drush_get_arguments, with the * current command table. * * Note that not all commands may be discoverable at the point-of-call, * since Drupal modules can ship commands as well, and they are * not available until after bootstrapping. * * drush_parse_command returns a normalized command descriptor, which * is an associative array. Some of its entries are: * - callback arguments: an array of arguments to pass to the calback. * - callback: the function to run. Usually, this is 'drush_command', which * will determine the primary hook for the function automatically. Only * specify a callback function if you need many commands to call the same * function (e.g. drush_print_file). * - invoke hooks: If TRUE (the default), Drush will invoke all of the pre and * post hooks for this command. Set to FALSE to suppress hooks. This setting * is ignored unless the command 'callback' is also set. * - primary function: Drush will copy the 'callback' parameter here if * necessary. This value should not be set explicitly; use 'callback' instead. * - description: description of the command. * - arguments: an array of arguments that are understood by the command. for help texts. * - required-arguments: The minimum number of arguments that are required, or TRUE if all are required. * - options: an array of options that are understood by the command. for help texts. * - global-options: a list of options from the set of Drush global options (@see: * drush_get_global_options()) that relate to this command. The help for these * options will be included in the help output for this command. * - examples: an array of examples that are understood by the command. for help texts. * - scope: one of 'system', 'project', 'site'. * - bootstrap: drupal bootstrap level (depends on Drupal major version). -1=no_bootstrap. * - core: Drupal major version required. * - drupal dependencies: drupal modules required for this command. * - drush dependencies: other drush command files required for this command. * - handle-remote-commands: set to TRUE if `drush @remote mycommand` should be executed * locally rather than remotely dispatched. When this mode is set, the target site * can be obtained via: * drush_get_context('DRUSH_TARGET_SITE_ALIAS') * - remote-tty: set to TRUE if Drush should force ssh to allocate a pseudo-tty * when this command is being called remotely. Important for interactive commands. * Remote commands that allocate a psedo-tty always print "Connection closed..." when done. * - strict-option-handling: set to TRUE if drush should strictly separate local command * cli options from the global options. Usually, drush allows global cli options and * command cli options to be interspersed freely on the commandline. For commands where * this flag is set, options are separated, with global options comming before the * command names, and command options coming after, like so: * drush --global-options command --command-options * In this mode, the command options are no longer available via drush_get_option(); * instead, they can be retrieved via: * $args = drush_get_original_cli_args_and_options(); * $args = drush_get_context('DRUSH_COMMAND_ARGS', array()); * In this case, $args will contain the command args and options literally, exactly as they * were entered on the command line, and in the same order as they appeared. * - 'outputformat': declares the data format to be used to render the * command result. In addition to the output format engine options * listed below, each output format type can take additional metadata * items that control the way that the output is rendered. See the * comment in each particular output format class for information. The * Drush core output format engines can be found in commands/core/outputformat. * - 'default': The default type to render output as. If declared, the * command should not print any output on its own, but instead should * return a data structure (usually an associative array) that can * be rendered by the output type selected. * - 'pipe-format': When the command is executed in --pipe mode, the * command output will be rendered by the format specified by the * pipe-format item instead of the default format. Note that in * either event, the user may specify the format to use via the * --format command-line option. * - 'formatted-filter': specifies a function callback that will be * used to filter the command result if the selected output formatter * is NOT declared to be machine-parsable. "table" is an example of * an output format that is not machine-parsable. * - 'parsable-filter': function callback that will be used to filter the * command result if the selected output formatter is declared to be * machine-parsable. "var_export" is an example of an output format that * is machine-parsable. * - 'output-data-type': An identifier representing the data structure that * the command returns. @see outputformat_drush_engine_outputformat() for * a description of the supported values. * - 'field-labels': A mapping from machine name to human-readable name * for all of the fields in a table-format command result. All * possible field names should appear in this list. * - 'fields-default': A list of the machine names of the fields that * should be displayed by default in tables. * - 'private-fields': A list of any fields that contain sensitive * information, such as passwords. By default, Drush will hide private * fields before printing the results to the console, but will include * them in backend invoke results. Use --show-passwords to display. * - 'column-widths': A mapping from field machine name to the column width * that should be used in table output. Drush will automatically * calculate the width of any field not listed here based on the length * of the data items in it. * - engines: declares information on Drush engines the command will load. * Available engines can vary by command type. * * @return bool|array * A command definition. */ function drush_parse_command() { $args = drush_get_arguments(); $command = FALSE; // Get a list of all implemented commands. $implemented = drush_get_commands(); if (!empty($args) && isset($implemented[$args[0]])) { $command = $implemented[$args[0]]; $arguments = array_slice($args, 1); } // We have found a command that matches. Set the appropriate values. if ($command) { // Special case. Force help command if --help option was specified. if (drush_get_option('help')) { $arguments = array($command['command']); $command = $implemented['helpsingle']; $command['arguments'] = $arguments; $command['allow-additional-options'] = TRUE; } else { _drush_prepare_command($command, $arguments); } drush_set_command($command); } return $command; } /** * Called by drush_parse_command(). If a command is dispatched * directly by drush_dispatch(), then drush_dispatch() will call * this function. */ function _drush_prepare_command(&$command, $arguments = array()) { // Drush overloads $command['arguments']; save the argument description if (!isset($command['argument-description'])) { $command['argument-description'] = $command['arguments']; } // Merge specified callback arguments, which precede the arguments passed on the command line. if (isset($command['callback arguments']) && is_array($command['callback arguments'])) { $arguments = array_merge($command['callback arguments'], $arguments); } $command['arguments'] = $arguments; } /** * Invoke a hook in all available command files that implement it. * * @see drush_command_invoke_all_ref() * * @param $hook * The name of the hook to invoke. * @param ... * Arguments to pass to the hook. * @return * An array of return values of the hook implementations. If commands return * arrays from their implementations, those are merged into one array. */ function drush_command_invoke_all() { $args = func_get_args(); if (count($args) == 1) { $args[] = NULL; } $reference_value = $args[1]; $args[1] = &$reference_value; return call_user_func_array('drush_command_invoke_all_ref', $args); } /** * A drush_command_invoke_all() that wants the first parameter to be passed by reference. * * @see drush_command_invoke_all() */ function drush_command_invoke_all_ref($hook, &$reference_parameter) { $args = func_get_args(); array_shift($args); // Insure that call_user_func_array can alter first parameter $args[0] = &$reference_parameter; $return = array(); $modules = drush_command_implements($hook); if ($hook != 'drush_invoke_alter') { // Allow modules to control the order of hook invocations drush_command_invoke_all_ref('drush_invoke_alter', $modules, $hook); } foreach ($modules as $module) { $function = $module .'_'. $hook; $result = call_user_func_array($function, $args); if (isset($result) && is_array($result)) { $return = array_merge_recursive($return, $result); } else if (isset($result)) { $return[] = $result; } } return $return; } /** * Determine which command files are implementing a hook. * * @param $hook * The name of the hook (e.g. "drush_help" or "drush_command"). * * @return * An array with the names of the command files which are implementing this hook. */ function drush_command_implements($hook) { $implementations[$hook] = array(); $list = drush_commandfile_list(); foreach ($list as $commandfile => $file) { if (drush_command_hook($commandfile, $hook)) { $implementations[$hook][] = $commandfile; } } return (array)$implementations[$hook]; } /** * @param string * name of command to check. * * @return boolean * TRUE if the given command has an implementation. */ function drush_is_command($command) { $commands = drush_get_commands(); return isset($commands[$command]); } /** * @param string * name of command or command alias. * * @return string * Primary name of command. */ function drush_command_normalize_name($command_name) { $commands = drush_get_commands(); return isset($commands[$command_name]) ? $commands[$command_name]['command'] : $command_name; } /** * Collect a list of all available drush command files. * * Scans the following paths for drush command files: * * - The "/path/to/drush/commands" folder. * - Folders listed in the 'include' option (see example.drushrc.php). * - The system-wide drush commands folder, e.g. /usr/share/drush/commands * - The ".drush" folder in the user's HOME folder. * - /drush and sites/all/drush in current Drupal site. * - Folders belonging to enabled modules in the current Drupal site. * * A Drush command file is a file that matches "*.drush.inc". * * @see drush_scan_directory() * * @return * An associative array whose keys and values are the names of all available * command files. */ function drush_commandfile_list() { return commandfiles_cache()->get(); } function _drush_find_commandfiles($phase, $phase_max = FALSE) { drush_log(dt("Find command files for phase !phase (max=!max)", array('!phase' => $phase, '!max' => (string)$phase_max)), LogLevel::DEBUG); if ($bootstrap = drush_get_bootstrap_object()) { $searchpath = $bootstrap->commandfile_searchpaths($phase, $phase_max); _drush_add_commandfiles($searchpath, $phase); annotationcommand_adapter_discover($searchpath, $phase, $phase_max); } } function _drush_add_commandfiles($searchpath, $phase = NULL, $reset = FALSE) { static $evaluated = array(); $needs_sort = FALSE; if (count($searchpath)) { if (!$reset) { // Assemble a cid specific to the bootstrap phase and searchpaths. // Bump $cf_version when making a change to a dev version of Drush // that invalidates the commandfile cache. $cf_version = 8; $cid = drush_get_cid('commandfiles-' . $phase, array(), array_merge($searchpath, array($cf_version))); $command_cache = drush_cache_get($cid); if (isset($command_cache->data)) { $cached_list = $command_cache->data; // If we want to temporarily ignore modules via 'ignored-modules', // then we need to take these out of the cache as well. foreach (drush_get_option_list('ignored-modules') as $ignored) { unset($cached_list[$ignored]); } } } // Build a list of all of the modules to attempt to load. // Start with any modules deferred from a previous phase. $list = commandfiles_cache()->deferred(); if (isset($cached_list)) { $list = array_merge($list, $cached_list); } else { // Scan for drush command files; add to list for consideration if found. foreach (array_unique($searchpath) as $path) { if (is_dir($path)) { $nomask = array_merge(drush_filename_blacklist(), drush_get_option_list('ignored-modules')); $dmv = DRUSH_MAJOR_VERSION; $files = drush_scan_directory($path, "/\.drush($dmv|)\.inc$/", $nomask); foreach ($files as $filename => $info) { $module = basename($filename); $module = preg_replace('/\.*drush[0-9]*\.inc/', '', $module); // Only try to bootstrap modules that we have never seen before. if (!array_key_exists($module, $evaluated) && file_exists($filename)) { $evaluated[$module] = TRUE; $list[$module] = Path::canonicalize($filename); } } } } if (isset($cid)) { drush_cache_set($cid, $list); } } // Check to see if the commandfile is valid for this version of Drupal // and is still present on filesystem (in case of cached commandfile list). foreach ($list as $module => $filename) { // Only try to require if the file exists. If not, a file from the // command file cache may not be available anymore, in which case // we rebuild the cache for this phase. if (file_exists($filename)) { // Avoid realpath() here as Drush commandfiles can have phar:// locations. $load_command = commandfiles_cache()->add($filename); if ($load_command) { $needs_sort = TRUE; } } elseif (!$reset) { _drush_add_commandfiles($searchpath, $phase, TRUE); $needs_sort = FALSE; } } if ($needs_sort) { commandfiles_cache()->sort(); } } } /** * Substrings to ignore during commandfile and site alias searching. */ function drush_filename_blacklist() { $blacklist = array('.', '..', 'drush_make', 'examples', 'tests', 'disabled', 'gitcache', 'cache'); for ($v=4; $v<=(DRUSH_MAJOR_VERSION)+3; ++$v) { if ($v != DRUSH_MAJOR_VERSION) { $blacklist[] = 'drush' . $v; } } $blacklist = array_merge($blacklist, drush_get_option_list('exclude')); return $blacklist; } /** * Conditionally include files based on the command used. * * Steps through each of the currently loaded commandfiles and * loads an optional commandfile based on the key. * * When a command such as 'pm-enable' is called, this * function will find all 'enable.pm.inc' files that * are present in each of the commandfile directories. */ function drush_command_include($command) { $include_files = drush_command_get_includes($command); foreach($include_files as $filename => $commandfile) { drush_log(dt('Including !filename', array('!filename' => $filename)), LogLevel::BOOTSTRAP); include_once($filename); } } function drush_command_get_includes($command) { $include_files = array(); $parts = explode('-', $command); $command = implode(".", array_reverse($parts)); $commandfiles = drush_commandfile_list(); $options = array(); foreach ($commandfiles as $commandfile => $file) { $filename = sprintf("%s/%s.inc", dirname($file), $command); if (file_exists($filename)) { $include_files[$filename] = $commandfile; } } return $include_files; } /** * Conditionally include default options based on the command used. */ function drush_command_default_options($command = NULL) { $command_default_options = drush_get_context('command-specific'); drush_command_set_command_specific($command_default_options, $command); } function drush_sitealias_command_default_options($site_record, $prefix, $command = NULL) { if (isset($site_record) && array_key_exists($prefix . 'command-specific', $site_record)) { drush_command_set_command_specific($site_record[$prefix . 'command-specific'], $command); } return FALSE; } function drush_command_set_command_specific_options($prefix, $command = NULL) { $command_default_options = drush_get_option($prefix . 'command-specific', array()); drush_command_set_command_specific($command_default_options, $command); } function drush_command_set_command_specific($command_default_options, $command = NULL) { if (!$command) { $command = drush_get_command(); } if ($command) { // Look for command-specific options for this command // keyed both on the command's primary name, and on each // of its aliases. $options_were_set = _drush_command_set_default_options($command_default_options, $command['command']); if (isset($command['aliases']) && count($command['aliases'])) { foreach ($command['aliases'] as $alias) { $options_were_set += _drush_command_set_default_options($command_default_options, $alias); } } // If we set or cleared any options, go back and re-bootstrap any global // options such as -y and -v. if (!empty($options_were_set)) { _drush_preflight_global_options(); } // If the command uses strict option handling, back out any global // options that were set. if ($command['strict-option-handling']) { $global_options = drush_get_global_options(); foreach ($options_were_set as $key) { if (array_key_exists($key, $global_options)) { if (!array_key_exists('context', $global_options[$key])) { $strict_options_warning =& drush_get_context('DRUSH_STRICT_OPTIONS_WARNING', array()); if (!array_key_exists($key, $strict_options_warning)) { drush_log(dt("Global option --!option not supported in command-specific options for command !command due to a limitation in strict option handling.", array('!option' => $key, '!command' => $command['command'])), LogLevel::WARNING); $strict_options_warning[$key] = TRUE; } } drush_unset_option($key, 'specific'); } } } } } function _drush_command_set_default_options($command_default_options, $command) { $options_were_set = array(); if (array_key_exists($command, $command_default_options)) { foreach ($command_default_options[$command] as $key => $value) { // We set command-specific options in their own context // that is higher precedence than the various config file // context, but lower than command-line options. if (!drush_get_option('no-' . $key, FALSE)) { drush_set_option($key, $value, 'specific'); $options_were_set[] = $key; } } } return $options_were_set; } /** * Return all of the command-specific options defined in the given * options set for the specified command name. Note that it is valid * to use the command name alias rather than the primary command name, * both in the parameter to this function, and in the options set. */ function drush_command_get_command_specific_options($options, $command_name, $prefix = '') { $result = array(); $command_name = drush_command_normalize_name($command_name); if (isset($options[$prefix . 'command-specific'])) { foreach ($options[$prefix . 'command-specific'] as $options_for_command => $values) { if ($command_name == drush_command_normalize_name($options_for_command)) { $result = array_merge($result, $values); } } } return $result; } /** * Return the original cli args and options, exactly as they * appeared on the command line, and in the same order. * Any command-specific options that were set will also * appear in this list, appended at the very end. * * The args and options returned are raw, and must be * escaped as necessary before use. */ function drush_get_original_cli_args_and_options($command = NULL) { $args = drush_get_context('DRUSH_COMMAND_ARGS', array()); $command_specific_options = drush_get_context('specific'); if ($command == NULL) { $command = drush_get_command(); } $command_options = ($command == NULL) ? array() : _drush_get_command_options($command); foreach ($command_specific_options as $key => $value) { if (!array_key_exists($key, $command_options)) { if (($value === TRUE) || (!isset($value))) { $args[] = "--$key"; } else { $args[] = "--$key=$value"; } } } return $args; } /** * Determine whether a command file implements a hook. * * @param $module * The name of the module (without the .module extension). * @param $hook * The name of the hook (e.g. "help" or "menu"). * @return * TRUE if the the hook is implemented. */ function drush_command_hook($commandfile, $hook) { return function_exists($commandfile . '_' . $hook); } /** * Check that a command is valid for the current bootstrap phase. * * @param $command * Command to check. Any errors will be added to the 'bootstrap_errors' element. * * @return * TRUE if command is valid. */ function drush_enforce_requirement_bootstrap_phase(&$command) { $valid = array(); $current_phase = drush_get_context('DRUSH_BOOTSTRAP_PHASE'); if ($command['bootstrap'] <= $current_phase) { return TRUE; } // TODO: provide description text for each bootstrap level so we can give // the user something more helpful and specific here. $command['bootstrap_errors']['DRUSH_COMMAND_INSUFFICIENT_BOOTSTRAP'] = dt('Command !command needs a higher bootstrap level to run - you will need to invoke drush from a more functional Drupal environment to run this command.', array('!command' => $command['command'])); } /** * Check that a command has its declared drush dependencies available or have no * dependencies. Drush dependencies are helpful when a command is invoking * another command, or implementing its API. * * @param $command * Command to check. Any errors will be added to the 'bootstrap_errors' element. * @return * TRUE if dependencies are met. */ function drush_enforce_requirement_drush_dependencies(&$command) { // If there are no drush dependencies, then do nothing. if (!empty($command['drush dependencies'])) { $commandfiles = drush_commandfile_list(); foreach ($command['drush dependencies'] as $dependency) { if (!isset($commandfiles[$dependency])) { $dt_args = array( '!command' => $command['command'], '!dependency' => "$dependency.drush.inc", ); $command['bootstrap_errors']['DRUSH_COMMANDFILE_DEPENDENCY_ERROR'] = dt('Command !command needs the following drush command file to run: !dependency.', $dt_args); return FALSE; } } } return TRUE; } /** * Check that a command is valid for the current major version of core. Handles * explicit version numbers and 'plus' numbers like 7+ (compatible with 7,8 ...). * * @param $command * Command to check. Any errors will be added to the 'bootstrap_errors' element. * * @return * TRUE if command is valid. */ function drush_enforce_requirement_core(&$command) { $major = drush_drupal_major_version(); if (!$core = $command['core']) { return TRUE; } foreach ($core as $compat) { if ($compat == $major) { return TRUE; } elseif (substr($compat, -1) == '+' && $major >= substr($compat, 0, strlen($compat)-1)) { return TRUE; } } $versions = array_pop($core); if (!empty($core)) { $versions = implode(', ', $core) . dt(' or ') . $versions; } $command['bootstrap_errors']['DRUSH_COMMAND_CORE_VERSION_ERROR'] = dt('Command !command requires Drupal core version !versions to run.', array('!command' => $command['command'], '!versions' => $versions)); } /** * Check if a shell alias exists for current request. If so, re-route to * core-execute and pass alias value along with rest of CLI arguments. */ function drush_shell_alias_replace($target_site_alias) { $escape = TRUE; $args = drush_get_arguments(); $argv = drush_get_context('argv'); $first = current($args); // @todo drush_get_option is awkward here. $shell_aliases = drush_get_context('shell-aliases', array()); if (isset($shell_aliases[$first])) { // Shell alias found for first argument in the request. $alias_value = $shell_aliases[$first]; if (!is_array($alias_value)) { // Shell aliases can have embedded variables such as {{@target}} and {{%root}} // that are replaced with the name of the target site alias, or the value of a // path alias defined in the target site alias record. We only support replacements // when the alias value is a string; if it is already broken out into an array, // then the values therein are used literally. $alias_variables = array( '{{@target}}' => '@none' ); if ($target_site_alias) { $alias_variables = array( '{{@target}}' => $target_site_alias ); $target = drush_sitealias_get_record($target_site_alias); foreach ($target as $key => $value) { if (!is_array($value)) { $alias_variables['{{' . $key . '}}'] = $value; } } if (array_key_exists('path-aliases', $target)) { foreach ($target['path-aliases'] as $key => $value) { // n.b. $key will contain something like "%root" or "%files". $alias_variables['{{' . $key . '}}'] = $value; } } } $alias_value = str_replace(array_keys($alias_variables), array_values($alias_variables), $alias_value); // Check for unmatched replacements $matches = array(); $match_result = preg_match('/{{[%@#]*[a-z0-9.]*}}/', $alias_value, $matches); if ($match_result) { $unmatched_replacements = implode(', ', $matches); $unmatched_replacements = preg_replace('/[{}]/', '', $unmatched_replacements); return drush_set_error('DRUSH_SHELL_ALIAS_UNMATCHED_REPLACEMENTS', dt('The shell alias @alias-name uses replacements "@unmatched". You must use this command with a site alias (e.g. `drush @myalias @alias-name ...`) that defines all of these variables.', array('@alias-name' => $first, '@unmatched' => $unmatched_replacements))); } if (substr($alias_value, 0, 1) == '!') { $alias_value = ltrim($alias_value, '!'); $alias_value = array('core-execute', $alias_value); $escape = FALSE; } else { // Respect quoting. See http://stackoverflow.com/questions/2202435/php-ex $alias_value = str_getcsv($alias_value, ' '); } } drush_log(dt('Shell alias found: !key => !value', array('!key' => $first, '!value' => implode(' ', $alias_value))), LogLevel::DEBUG); $pos = array_search($first, $argv); $number = 1; if ($target_site_alias && ($argv[$pos - 1] == $target_site_alias)) { --$pos; ++$number; } array_splice($argv, $pos, $number, $alias_value); if (!$escape) { drush_set_option('escape', FALSE); } drush_set_context('argv', $argv); drush_parse_args(); _drush_preflight_global_options(); } } function commandfiles_cache() { static $commandfiles_cache = NULL; if (!isset($commandfiles_cache)) { $commandfiles_cache = new Drush\Command\Commandfiles(); } return $commandfiles_cache; } . The shell completion scripts should call * "drush complete ", where is the full command line, which we take * as input and use to produce a list of possible completions for the * current/next word, separated by newlines. Typically, when multiple * completions are returned the shell will display them to the user in a concise * format - but when a single completion is returned it will autocomplete. * * We provide completion for site aliases, commands, shell aliases, options, * engines and arguments. Displaying all of these when the last word has no * characters yet is not useful, as there are too many items. Instead we filter * the possible completions based on position, in a similar way to git. * For example: * - We only display site aliases and commands if one is not already present. * - We only display options if the user has already entered a hyphen. * - We only display global options before a command is entered, and we only * display command specific options after the command (Drush itself does not * care about option placement, but this approach keeps things more concise). * * Below is typical output of complete in different situations. Tokens in square * brackets are optional, and [word] will filter available options that start * with the same characters, or display all listed options if empty. * drush --[word] : Output global options * drush [word] : Output site aliases, sites, commands and shell aliases * drush [@alias] [word] : Output commands * drush [@alias] command [word] : Output command specific arguments * drush [@alias] command --[word] : Output command specific options * * Because the purpose of autocompletion is to make the command line more * efficient for users we need to respond quickly with the list of completions. * To do this, we call drush_complete() early in the Drush bootstrap, and * implement a simple caching system. * * To generate the list of completions, we set up the Drush environment as if * the command was called on it's own, parse the command using the standard * Drush functions, bootstrap the site (if any) and collect available * completions from various sources. Because this can be somewhat slow, we cache * the results. The cache strategy aims to balance accuracy and responsiveness: * - We cache per site, if a site is available. * - We generate (and cache) everything except arguments at the same time, so * subsequent completions on the site don't need any bootstrap. * - We generate and cache arguments on-demand, since these can often be * expensive to generate. Arguments are also cached per-site. * * For argument completions, commandfiles can implement * COMMANDFILE_COMMAND_complete() returning an array containing a key 'values' * containing an array of all possible argument completions for that command. * For example, return array('values' => array('aardvark', 'aardwolf')) offers * the words 'aardvark' and 'aardwolf', or will complete to 'aardwolf' if the * letters 'aardw' are already present. Since command arguments are cached, * commandfiles can bootstrap a site or perform other somewhat time consuming * activities to retrieve the list of possible arguments. Commands can also * clear the cache (or just the "arguments" cache for their command) when the * completion results have likely changed - see drush_complete_cache_clear(). * * Commandfiles can also return a special optional element in their array with * the key 'files' that contains an array of patterns/flags for the glob() * function. These are used to produce file and directory completions (the * results of these are not cached, since this is a fast operation). * See http://php.net/glob for details of valid patterns and flags. * For example the following will complete the command arguments on all * directories, as well as files ending in tar.gz: * return array( * 'files' => array( * 'directories' => array( * 'pattern' => '*', * 'flags' => GLOB_ONLYDIR, * ), * 'tar' => array( * 'pattern' => '*.tar.gz', * ), * ), * ); * * To check completion results without needing to actually trigger shell * completion, you can call this manually using a command like: * * drush --early=includes/complete.inc [--complete-debug] drush [@alias] [command]... * * If you want to simulate the results of pressing tab after a space (i.e. * and empty last word, include '' on the end of your command: * * drush --early=includes/complete.inc [--complete-debug] drush '' */ /** * Produce autocomplete output. * * Determine position (is there a site-alias or command set, and are we trying * to complete an option). Then produce a list of completions for the last word * and output them separated by newlines. */ function drush_early_complete() { // We use a distinct --complete-debug option to avoid unwanted debug messages // being printed when users use this option for other purposes in the command // they are trying to complete. drush_set_option(LogLevel::DEBUG, FALSE); if (drush_get_option('complete-debug', FALSE)) { drush_set_context('DRUSH_DEBUG', TRUE); } // Set up as if we were running the command, and attempt to parse. $argv = drush_complete_process_argv(); if ($alias = drush_get_context('DRUSH_TARGET_SITE_ALIAS')) { $set_sitealias_name = $alias; $set_sitealias = drush_sitealias_get_record($alias); } // Arguments have now had site-aliases and options removed, so we take the // first item as our command. We need to know if the command is valid, so that // we know if we are supposed to complete an in-progress command name, or // arguments for a command. We do this by checking against our per-site cache // of command names (which will only bootstrap if the cache needs to be // regenerated), rather than drush_parse_command() which always requires a // site bootstrap. $arguments = drush_get_arguments(); $set_command_name = NULL; if (isset($arguments[0]) && in_array($arguments[0] . ' ', drush_complete_get('command-names'))) { $set_command_name = $arguments[0]; } // We unset the command if it is "help" but that is not explicitly found in // args, since Drush sets the command to "help" if no command is specified, // which prevents completion of global options. if ($set_command_name == 'help' && !array_search('help', $argv)) { $set_command_name = NULL; } // Determine the word we are trying to complete, and if it is an option. $last_word = end($argv); $word_is_option = FALSE; if (!empty($last_word) && $last_word[0] == '-') { $word_is_option = TRUE; $last_word = ltrim($last_word, '-'); } $completions = array(); if (!$set_command_name) { // We have no command yet. if ($word_is_option) { // Include global option completions. $completions += drush_hyphenate_options(drush_complete_match($last_word, drush_complete_get('options'))); } else { if (empty($set_sitealias_name)) { // Include site alias completions. $completions += drush_complete_match($last_word, drush_complete_get('site-aliases')); } // Include command completions. $completions += drush_complete_match($last_word, drush_complete_get('command-names')); } } else { if ($last_word == $set_command_name) { // The user just typed a valid command name, but we still do command // completion, as there may be other commands that start with the detected // command (e.g. "make" is a valid command, but so is "make-test"). // If there is only the single matching command, this will include in the // completion list so they get a space inserted, confirming it is valid. $completions += drush_complete_match($last_word, drush_complete_get('command-names')); } else if ($word_is_option) { // Include command option completions. $completions += drush_hyphenate_options(drush_complete_match($last_word, drush_complete_get('options', $set_command_name))); } else { // Include command argument completions. $argument_completion = drush_complete_get('arguments', $set_command_name); if (isset($argument_completion['values'])) { $completions += drush_complete_match($last_word, $argument_completion['values']); } if (isset($argument_completion['files'])) { $completions += drush_complete_match_file($last_word, $argument_completion['files']); } } } if (!empty($completions)) { sort($completions); return implode("\n", $completions); } return TRUE; } /** * This function resets the raw arguments so that Drush can parse the command as * if it was run directly. The shell complete command passes the * full command line as an argument, and the --early and --complete-debug * options have to come before that, and the "drush" bash script will add a * --php option on the end, so we end up with something like this: * * /path/to/drush.php --early=includes/complete.inc [--complete-debug] drush [@alias] [command]... --php=/usr/bin/php * * Note that "drush" occurs twice, and also that the second occurrence could be * an alias, so we can't easily use it as to detect the start of the actual * command. Hence our approach is to remove the initial "drush" and then any * options directly following that - what remains is then the command we need * to complete - i.e.: * * drush [@alias] [command]... * * Note that if completion is initiated following a space an empty argument is * added to argv. So in that case argv looks something like this: * array ( * '0' => '/path/to/drush.php', * '1' => '--early=includes/complete.inc', * '2' => 'drush', * '3' => 'topic', * '4' => '', * '5' => '--php=/usr/bin/php', * ); * * @return $args * Array of arguments (argv), excluding the initial command and options * associated with the complete call. * array ( * '0' => 'drush', * '1' => 'topic', * '2' => '', * ); */ function drush_complete_process_argv() { $argv = drush_get_context('argv'); // Remove the first argument, which will be the "drush" command. array_shift($argv); while (substr($arg = array_shift($argv), 0, 2) == '--') { // We remove all options, until we get to a non option, which // marks the start of the actual command we are trying to complete. } // Replace the initial argument. array_unshift($argv, $arg); // Remove the --php option at the end if exists (added by the "drush" shell // script that is called when completion is requested). if (substr(end($argv), 0, 6) == '--php=') { array_pop($argv); } drush_set_context('argv', $argv); drush_set_command(NULL); // Reparse arguments, site alias, and command. drush_parse_args(); // Ensure the base environment is configured, so tests look in the correct // places. _drush_preflight_base_environment(); // Check for and record any site alias. drush_sitealias_check_arg(); drush_sitealias_check_site_env(); // We might have just changed our root--run drush_select_bootstrap_class() again. $bootstrap = drush_select_bootstrap_class(); // Return the new argv for easy reference. return $argv; } /** * Retrieves the appropriate list of candidate completions, then filters this * list using the last word that we are trying to complete. * * @param string $last_word * The last word in the argument list (i.e. the subject of completion). * @param array $values * Array of possible completion values to filter. * * @return array * Array of candidate completions that start with the same characters as the * last word. If the last word is empty, return all candidates. */ function drush_complete_match($last_word, $values) { // Using preg_grep appears to be faster that strpos with array_filter/loop. return preg_grep('/^' . preg_quote($last_word, '/') . '/', $values); } /** * Retrieves the appropriate list of candidate file/directory completions, * filtered by the last word that we are trying to complete. * * @param string $last_word * The last word in the argument list (i.e. the subject of completion). * @param array $files * Array of file specs, each with a pattern and flags subarray. * * @return array * Array of candidate file/directory completions that start with the same * characters as the last word. If the last word is empty, return all * candidates. */ function drush_complete_match_file($last_word, $files) { $return = array(); if ($last_word[0] == '~') { // Complete does not do tilde expansion, so we do it here. // We shell out (unquoted) to expand the tilde. drush_shell_exec('echo ' . $last_word); return drush_shell_exec_output(); } $dir = ''; if (substr($last_word, -1) == '/' && is_dir($last_word)) { // If we exactly match a trailing directory, then we use that as the base // for the listing. We only do this if a trailing slash is present, since at // this stage it is still possible there are other directories that start // with this string. $dir = $last_word; } else { // Otherwise we discard the last part of the path (this is matched against // the list later), and use that as our base. $dir = dirname($last_word); if (empty($dir) || $dir == '.' && $last_word != '.' && substr($last_word, 0, 2) != './') { // We are looking at the current working directory, so unless the user is // actually specifying a leading dot we leave the path empty. $dir = ''; } else { // In all other cases we need to add a trailing slash. $dir .= '/'; } } foreach ($files as $spec) { // We always include GLOB_MARK, as an easy way to detect directories. $flags = GLOB_MARK; if (isset($spec['flags'])) { $flags = $spec['flags'] | GLOB_MARK; } $listing = glob($dir . $spec['pattern'], $flags); $return = array_merge($return, drush_complete_match($last_word, $listing)); } // If we are returning a single item (which will become part of the final // command), we need to use the full path, and we need to escape it // appropriately. if (count($return) == 1) { // Escape common shell metacharacters (we don't use escapeshellarg as it // single quotes everything, even when unnecessary). $item = array_pop($return); $item = preg_replace('/[ |&;()<>]/', "\\\\$0", $item); if (substr($item, -1) !== '/') { // Insert a space after files, since the argument is complete. $item = $item . ' '; } $return = array($item); } else { $firstchar = TRUE; if ($last_word[0] == '/') { // If we are working with absolute paths, we need to check if the first // character of all the completions matches. If it does, then we pass a // full path for each match, so the shell completes as far as it can, // matching the behaviour with relative paths. $pos = strlen($last_word); foreach ($return as $id => $item) { if ($item[$pos] !== $return[0][$pos]) { $firstchar = FALSE; continue; } } } foreach ($return as $id => $item) { // For directories we leave the path alone. $slash_pos = strpos($last_word, '/'); if ($slash_pos === 0 && $firstchar) { // With absolute paths where completions share initial characters, we // pass in a resolved path. $return[$id] = realpath($item); } else if ($slash_pos !== FALSE && $dir != './') { // For files, we pass only the file name, ignoring the false match when // the user is using a single dot relative path. $return[$id] = basename($item); } } } return $return; } /** * Simple helper function to ensure options are properly hyphenated before we * return them to the user (we match against the non-hyphenated versions * internally). * * @param array $options * Array of unhyphenated option names. * * @return array * Array of hyphenated option names. */ function drush_hyphenate_options($options) { foreach ($options as $key => $option) { $options[$key] = '--' . ltrim($option, '--'); } return $options; } /** * Retrieves from cache, or generates a listing of completion candidates of a * specific type (and optionally, command). * * @param string $type * String indicating type of completions to return. * See drush_complete_rebuild() for possible keys. * @param string $command * An optional command name if command specific completion is needed. * * @return array * List of candidate completions. */ function drush_complete_get($type, $command = NULL) { static $complete; if (empty($command)) { // Quick return if we already have a complete static cache. if (!empty($complete[$type])) { return $complete[$type]; } // Retrieve global items from a non-command specific cache, or rebuild cache // if needed. $cache = drush_cache_get(drush_complete_cache_cid($type), 'complete'); if (isset($cache->data)) { return $cache->data; } $complete = drush_complete_rebuild(); return $complete[$type]; } // Retrieve items from a command specific cache. $cache = drush_cache_get(drush_complete_cache_cid($type, $command), 'complete'); if (isset($cache->data)) { return $cache->data; } // Build argument cache - built only on demand. if ($type == 'arguments') { return drush_complete_rebuild_arguments($command); } // Rebuild cache of general command specific items. if (empty($complete)) { $complete = drush_complete_rebuild(); } if (!empty($complete['commands'][$command][$type])) { return $complete['commands'][$command][$type]; } return array(); } /** * Rebuild and cache completions for everything except command arguments. * * @return array * Structured array of completion types, commands and candidate completions. */ function drush_complete_rebuild() { $complete = array(); // Bootstrap to the site level (if possible) - commands may need to check // the bootstrap level, and perhaps bootstrap higher in extraordinary cases. drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION); $commands = drush_get_commands(); foreach ($commands as $command_name => $command) { // Add command options and suboptions. $options = array_keys($command['options']); foreach ($command['sub-options'] as $option => $sub_options) { $options = array_merge($options, array_keys($sub_options)); } $complete['commands'][$command_name]['options'] = $options; } // We treat shell aliases as commands for the purposes of completion. $complete['command-names'] = array_merge(array_keys($commands), array_keys(drush_get_context('shell-aliases', array()))); $site_aliases = _drush_sitealias_all_list(); // TODO: Figure out where this dummy @0 alias is introduced. unset($site_aliases['@0']); $complete['site-aliases'] = array_keys($site_aliases); $complete['options'] = array_keys(drush_get_global_options()); // We add a space following all completes. Eventually there may be some // items (e.g. options that we know need values) where we don't add a space. array_walk_recursive($complete, 'drush_complete_trailing_space'); drush_complete_cache_set($complete); return $complete; } /** * Helper callback function that adds a trailing space to completes in an array. */ function drush_complete_trailing_space(&$item, $key) { if (!is_array($item)) { $item = (string)$item . ' '; } } /** * Rebuild and cache completions for command arguments. * * @param string $command * A specific command to retrieve and cache arguments for. * * @return array * Structured array of candidate completion arguments, keyed by the command. */ function drush_complete_rebuild_arguments($command) { // Bootstrap to the site level (if possible) - commands may need to check // the bootstrap level, and perhaps bootstrap higher in extraordinary cases. drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_SITE); $commands = drush_get_commands(); $command_info = $commands[$command]; if ($callback = $command_info['annotated-command-callback']) { list($classname, $method) = $callback; $commandInfo = new CommandInfo($classname, $method); if ($callable = $commandInfo->getAnnotation('complete')) { $result = call_user_func($callable); } } else { $hook = str_replace("-", "_", $command_info['command-hook']); $result = drush_command_invoke_all($hook . '_complete'); } if (isset($result['values'])) { // We add a space following all completes. Eventually there may be some // items (e.g. comma separated arguments) where we don't add a space. array_walk($result['values'], 'drush_complete_trailing_space'); } $complete = array( 'commands' => array( $command => array( 'arguments' => $result, ) ) ); drush_complete_cache_set($complete); return $complete['commands'][$command]['arguments']; } /** * Stores caches for completions. * * @param $complete * A structured array of completions, keyed by type, including a 'commands' * type that contains all commands with command specific completions keyed by * type. The array does not need to include all types - used by * drush_complete_rebuild_arguments(). */ function drush_complete_cache_set($complete) { foreach ($complete as $type => $values) { if ($type == 'commands') { foreach ($values as $command_name => $command) { foreach ($command as $command_type => $command_values) { drush_cache_set(drush_complete_cache_cid($command_type, $command_name), $command_values, 'complete', DRUSH_CACHE_TEMPORARY); } } } else { drush_cache_set(drush_complete_cache_cid($type), $values, 'complete', DRUSH_CACHE_TEMPORARY); } } } /** * Generate a cache id. * * @param $type * The completion type. * @param $command * The command name (optional), if completions are command specific. * * @return string * Cache id. */ function drush_complete_cache_cid($type, $command = NULL) { // For per-site caches, we include the site root and uri/path in the cache id // hash. These are quick to determine, and prevents a bootstrap to site just // to get a validated root and URI. Because these are not validated, there is // the possibility of cache misses/ but they should be rare, since sites are // normally referred to the same way (e.g. a site alias, or using the current // directory), at least within a single command completion session. // We also static cache them, since we may get differing results after // bootstrap, which prevents the caches from being found on the next call. static $root, $site; if (empty($root)) { $root = drush_get_option(array('r', 'root'), drush_locate_root()); $site = drush_get_option(array('l', 'uri'), drush_site_path()); } return drush_get_cid('complete', array(), array($type, $command, $root, $site)); } realpath($config), '!context' => $context)), LogLevel::BOOTSTRAP); $ret = @include_once($config); if ($ret === FALSE) { drush_log(dt('Cannot open drushrc "!config", ignoring.', array('!config' => realpath($config))), LogLevel::WARNING); return FALSE; } if (!empty($options) || !empty($aliases) || !empty($command_specific) || !empty($override)) { $options = array_merge(drush_get_context($context), $options); $options['config-file'] = realpath($config); unset($options['site-aliases']); $options['command-specific'] = array_merge(isset($command_specific) ? $command_specific : array(), isset($options['command-specific']) ? $options['command-specific'] : array()); drush_set_config_options($context, $options, $override); } } } } function drush_set_config_options($context, $options, $override = array()) { // Copy 'config-file' into 'context-path', converting to an array to hold multiple values if necessary if (isset($options['config-file'])) { if (isset($options['context-path'])) { $options['context-path'] = array_merge(array($options['config-file']), is_array($options['context-path']) ? $options['context-path'] : array($options['context-path'])); } else { $options['context-path'] = $options['config-file']; } } // Take out $aliases and $command_specific options drush_set_config_special_contexts($options); drush_set_context($context, $options); } /** * For all global options with a short form, convert all options in the option * array that use the short form into the long form. */ function drush_expand_short_form_options(&$options) { foreach (drush_get_global_options() as $name => $info) { if (is_array($info)) { // For any option with a short form, check to see if the short form was set in the // options. If it was, then rename it to its long form. if (array_key_exists('short-form', $info) && array_key_exists($info['short-form'], $options)) { if (!array_key_exists($name, $options) || !array_key_exists('merge-pathlist', $info)) { $options[$name] = $options[$info['short-form']]; } else { $options[$name] = array_merge((array)$options[$name], (array)$options[$info['short-form']]); } unset($options[$info['short-form']]); } } } } /** * There are certain options such as 'site-aliases' and 'command-specific' * that must be merged together if defined in multiple drush configuration * files. If we did not do this merge, then the last configuration file * that defined any of these properties would overwrite all of the options * that came before in previously-loaded configuration files. We place * all of them into their own context so that this does not happen. */ function drush_set_config_special_contexts(&$options) { if (isset($options) && is_array($options)) { $has_command_specific = array_key_exists('command-specific', $options); // Change the keys of the site aliases from 'alias' to '@alias' if (array_key_exists('site-aliases', $options)) { $user_aliases = $options['site-aliases']; $options['site-aliases'] = array(); foreach ($user_aliases as $alias_name => $alias_value) { if (substr($alias_name,0,1) != '@') { $alias_name = "@$alias_name"; } $options['site-aliases'][$alias_name] = $alias_value; } } // Expand -s into --simulate, etc. drush_expand_short_form_options($options); foreach (drush_get_global_options() as $name => $info) { if (is_array($info)) { // For any global option with the 'merge-pathlist' or 'merge-associative' flag, set its // value in the specified context. The option is 'merged' because we // load $options with the value from the context prior to including the // configuration file. If the configuration file sets $option['special'][] = 'value', // then the configuration will be merged. $option['special'] = array(...), on the // other hand, will replace rather than merge the values together. if ((array_key_exists($name, $options)) && (array_key_exists('merge', $info) || (array_key_exists('merge-pathlist', $info) || array_key_exists('merge-associative', $info)))) { $context = array_key_exists('context', $info) ? $info['context'] : $name; $cache =& drush_get_context($context); $value = $options[$name]; if (!is_array($value) && array_key_exists('merge-pathlist', $info)) { $value = explode(PATH_SEPARATOR, $value); } if (array_key_exists('merge-associative', $info)) { foreach ($value as $subkey => $subvalue) { $cache[$subkey] = array_merge(isset($cache[$subkey]) ? $cache[$subkey] : array(), $subvalue); } } else { $cache = array_unique(array_merge($cache, $value)); } // Once we have moved the option to its special context, we // can remove it from its option context -- unless 'propagate-cli-value' // is set, in which case we need to let it stick around in options // in case it is needed in backend invoke. if (!array_key_exists('propagate-cli-value', $info)) { unset($options[$name]); } } } } // If command-specific options were set and if we already have // a command, then apply the command-specific options immediately. if ($has_command_specific) { drush_command_default_options(); } } } /** * Set a specific context. * * @param context * Any of the default defined contexts. * @param value * The value to store in the context * * @return * An associative array of the settings specified in the request context. */ function drush_set_context($context, $value) { $cache =& drush_get_context($context); $cache = $value; return $value; } /** * Return a specific context, or the whole context cache * * This function provides a storage mechanism for any information * the currently running process might need to communicate. * * This avoids the use of globals, and constants. * * Functions that operate on the context cache, can retrieve a reference * to the context cache using : * $cache = &drush_get_context($context); * * This is a private function, because it is meant as an internal * generalized API for writing static cache functions, not as a general * purpose function to be used inside commands. * * Code that modifies the reference directly might have unexpected consequences, * such as modifying the arguments after they have already been parsed and dispatched * to the callbacks. * * @param context * Optional. Any of the default defined contexts. * * @return * If context is not supplied, the entire context cache will be returned. * Otherwise only the requested context will be returned. * If the context does not exist yet, it will be initialized to an empty array. */ function &drush_get_context($context = NULL, $default = NULL) { static $cache = array(); if (isset($context)) { if (!isset($cache[$context])) { $default = !isset($default) ? array() : $default; $cache[$context] = $default; } return $cache[$context]; } return $cache; } /** * Set the arguments passed to the drush.php script. * * This function will set the 'arguments' context of the current running script. * * When initially called by drush_parse_args, the entire list of arguments will * be populated. Once the command is dispatched, this will be set to only the remaining * arguments to the command (i.e. the command name is removed). * * @param arguments * Command line arguments, as an array. */ function drush_set_arguments($arguments) { drush_set_context('arguments', $arguments); } /** * Gets the command line arguments passed to Drush. * * @return array * An indexed array of arguments. Until Drush has dispatched the command, the * array will include the command name as the first element. After that point * the array will not include the command name. * * @see drush_set_arguments() */ function drush_get_arguments() { return drush_get_context('arguments'); } /** * Set the command being executed. * * Drush_dispatch will set the correct command based on it's * matching of the script arguments retrieved from drush_get_arguments * to the implemented commands specified by drush_get_commands. * * @param * A numerically indexed array of command components. */ function drush_set_command($command) { drush_set_context('command', $command); } /** * Return the command being executed. */ function drush_get_command() { return drush_get_context('command'); } /** * Get the value for an option. * * If the first argument is an array, then it checks whether one of the options * exists and return the value of the first one found. Useful for allowing both * -h and --host-name * * @param option * The name of the option to get * @param default * Optional. The value to return if the option has not been set * @param context * Optional. The context to check for the option. If this is set, only this context will be searched. */ function drush_get_option($option, $default = NULL, $context = NULL) { $value = NULL; if ($context) { // We have a definite context to check for the presence of an option. $value = _drush_get_option($option, drush_get_context($context)); } else { // We are not checking a specific context, so check them in a predefined order of precedence. $contexts = drush_context_names(); foreach ($contexts as $context) { $value = _drush_get_option($option, drush_get_context($context)); if ($value !== NULL) { return $value; } } } if ($value !== NULL) { return $value; } return $default; } /** * Get the value for an option and return it as a list. If the * option in question is passed on the command line, its value should * be a comma-separated list (e.g. --flag=1,2,3). If the option * was set in a drushrc.php file, then its value may be either a * comma-separated list or an array of values (e.g. $option['flag'] = array('1', '2', '3')). * * @param option * The name of the option to get * @param default * Optional. The value to return if the option has not been set * @param context * Optional. The context to check for the option. If this is set, only this context will be searched. */ function drush_get_option_list($option, $default = array(), $context = NULL) { $result = drush_get_option($option, $default, $context); if (!is_array($result)) { $result = array_map('trim', array_filter(explode(',', $result))); } return $result; } /** * Get the value for an option, but first checks the provided option overrides. * * The feature of drush_get_option that allows a list of option names * to be passed in an array is NOT supported. * * @param option_overrides * An array to check for values before calling drush_get_option. * @param option * The name of the option to get. * @param default * Optional. The value to return if the option has not been set. * @param context * Optional. The context to check for the option. If this is set, only this context will be searched. * */ function drush_get_option_override($option_overrides, $option, $default = NULL, $context = NULL) { return drush_sitealias_get_option($option_overrides, $option, $default, '', $context); } /** * Get an option out of the specified alias. If it has not been * set in the alias, then get it via drush_get_option. * * @param site_alias_record * An array of options for an alias record. * @param option * The name of the option to get. * @param default * Optional. The value to return if the option does not exist in the site record and has not been set in a context. * @param context * Optional. The context to check for the option. If this is set, only this context will be searched. */ function drush_sitealias_get_option($site_alias_record, $option, $default = NULL, $prefix = '', $context = NULL) { if (is_array($site_alias_record) && array_key_exists($option, $site_alias_record)) { return $site_alias_record[$option]; } else { return drush_get_option($prefix . $option, $default, $context); } } /** * Get all of the values for an option in every context. * * @param option * The name of the option to get * @return * An array whose key is the context name and value is * the specific value for the option in that context. */ function drush_get_context_options($option, $flatten = FALSE) { $result = array(); $contexts = drush_context_names(); foreach ($contexts as $context) { $value = _drush_get_option($option, drush_get_context($context)); if ($value !== NULL) { if ($flatten && is_array($value)) { $result = array_merge($value, $result); } else { $result[$context] = $value; } } } return $result; } /** * Retrieves a collapsed list of all options. */ function drush_get_merged_options() { $contexts = drush_context_names(); $cache = drush_get_context(); $result = array(); foreach (array_reverse($contexts) as $context) { if (array_key_exists($context, $cache)) { $result = array_merge($result, $cache[$context]); } } return $result; } /** * Retrieves a collapsed list of all options * with a specified prefix. */ function drush_get_merged_prefixed_options($prefix) { $merged_options = drush_get_merged_options(); $result = array(); foreach ($merged_options as $key => $value) { if ($prefix == substr($key, 0, strlen($prefix))) { $result[substr($key, strlen($prefix))] = $value; } } return $result; } /** * Helper function to recurse through possible option names */ function _drush_get_option($option, $context) { if (is_array($option)) { foreach ($option as $current) { $current_value = _drush_get_option($current, $context); if (isset($current_value)) { return $current_value; } } } elseif (array_key_exists('no-' . $option, $context)) { return FALSE; } elseif (array_key_exists($option, $context)) { return $context[$option]; } return NULL; } /** * Set an option in one of the option contexts. * * @param option * The option to set. * @param value * The value to set it to. * @param context * Optional. Which context to set it in. * @return * The value parameter. This allows for neater code such as * $myvalue = drush_set_option('http_host', $_SERVER['HTTP_HOST']); * Without having to constantly type out the value parameter. */ function drush_set_option($option, $value, $context = 'process') { $cache =& drush_get_context($context); $cache[$option] = $value; return $value; } /** * A small helper function to set the value in the default context */ function drush_set_default($option, $value) { return drush_set_option($option, $value, 'default'); } /** * Remove a setting from a specific context. * * @param * Option to be unset * @param * Context in which to unset the value in. */ function drush_unset_option($option, $context = NULL) { if ($context != NULL) { $cache =& drush_get_context($context); if (array_key_exists($option, $cache)) { unset($cache[$option]); } } else { $contexts = drush_context_names(); foreach ($contexts as $context) { drush_unset_option($option, $context); } } } /** * Save the settings in a specific context to the applicable configuration file * This is useful is you want certain settings to be available automatically the next time a command is executed. * * @param $context * The context to save */ function drush_save_config($context) { $filename = _drush_config_file($context); if (is_array($filename)) { $filename = $filename[0]; } if ($filename) { $cache = drush_get_context($context); $fp = fopen($filename, "w+"); if (!$fp) { return drush_set_error('DRUSH_PERM_ERROR', dt('Drushrc (!filename) could not be written', array('!filename' => $filename))); } else { fwrite($fp, " $value) { $line = "\n\$options['$key'] = ". var_export($value, TRUE) .';'; fwrite($fp, $line); } fwrite($fp, "\n"); fclose($fp); drush_log(dt('Drushrc file (!filename) was written successfully', array('!filename' => $filename))); return TRUE; } } return FALSE; } $data) { if (is_array($data)) { $new_keys = array(); // $data can't have keys that are a prefix of other keys to // prevent a corrupted result in the below calls to str_replace(). // To avoid this we will use a zero padded indexed array of the values of $data. $pad_length = strlen((string)count(array_values($data))); foreach (array_values($data) as $i => $value) { if (!is_numeric($value)) { $value = "'".$value."'"; } $new_keys[$key . '_' . str_pad($i, $pad_length, '0', STR_PAD_LEFT)] = $value; } $where = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $where); unset($args[$key]); $args += $new_keys; } else if (!is_numeric($data)) { $args[$key] = "'".$data."'"; } } foreach ($args as $key => $data) { $where = str_replace($key, $data, $where); } return $where; } /** * A db_select() that works for any version of Drupal. * * @param $table * String. The table to operate on. * @param $fields * Array or string. Fields affected in this operation. Valid string values are '*' or a single column name. * @param $where * String. WHERE snippet for the operation. It uses named placeholders. see @_drush_replace_query_placeholders() * @param $args * Array. Arguments for the WHERE snippet. * @param $start * Int. Value for OFFSET. * @param $length * Int. Value for LIMIT. * @param $order_by_field * String. Database column to order by. * @param $order_by_direction * ('ASC', 'DESC'). Ordering direction. * @return * A database resource. */ function drush_db_select($table, $fields = '*', $where = NULL, $args = NULL, $start = NULL, $length = NULL, $order_by_field = NULL, $order_by_direction = 'ASC') { if (drush_drupal_major_version() >= 7) { if (!is_array($fields)) { if ($fields == '*') { $fields = array(); } else { $fields = array($fields); } } $query = db_select($table, $table) ->fields($table, $fields); if (!empty($where)) { $query = $query->where($where, $args); } if (isset($order_by_field)) { $query = $query->orderBy($order_by_field, $order_by_direction); } if (isset($length)) { $query = $query->range($start, $length); } return $query->execute(); } else { if (is_array($fields)) { $fields = implode(', ', $fields); } $query = "SELECT $fields FROM {{$table}}"; if (!empty($where)) { $where = _drush_replace_query_placeholders($where, $args); $query .= " WHERE ".$where; } if (isset($order_by_field)) { $query .= " ORDER BY $order_by_field $order_by_direction"; } if (isset($length)) { $sql = drush_sql_get_class(); $db_scheme = $sql->scheme(); if ($db_scheme == 'oracle') return db_query_range($query, $start, $length); else { $limit = " LIMIT $length"; if (isset($start)) { $limit .= " OFFSET $start"; } $query .= $limit; } } return db_query($query, $args); } } /** * A db_delete() that works for any version of Drupal. * * @param $table * String. The table to operate on. * @param $where * String. WHERE snippet for the operation. It uses named placeholders. see @_drush_replace_query_placeholders() * @param $args * Array. Arguments for the WHERE snippet. * @return * Affected rows (except on D7+mysql without a WHERE clause - returns TRUE) or FALSE. */ function drush_db_delete($table, $where = NULL, $args = NULL) { if (drush_drupal_major_version() >= 7) { if (!empty($where)) { $query = db_delete($table)->where($where, $args); return $query->execute(); } else { return db_truncate($table)->execute(); } } else { $query = "DELETE FROM {{$table}}"; if (!empty($where)) { $where = _drush_replace_query_placeholders($where, $args); $query .= ' WHERE '.$where; } if (!db_query($query, $args)) { return FALSE; } return db_affected_rows(); } } /** * A db_result() that works consistently for any version of Drupal. * * @param * A Database result object. */ function drush_db_result($result) { switch (drush_drupal_major_version()) { case 6: return db_result($result); case 7: default: return $result->fetchField(); } } /** * A db_fetch_object() that works for any version of Drupal. * * @param * A Database result object. */ function drush_db_fetch_object($result) { return drush_drupal_major_version() >= 7 ? $result->fetchObject() : db_fetch_object($result); } /** * @} End of "defgroup dbfunctions". */ get_version($drupal_root); } } } return $version; } function drush_drupal_cache_clear_all() { if (drush_drupal_major_version() >= 8) { drush_invoke_process('@self', 'cache-rebuild'); } else { drush_invoke_process('@self', 'cache-clear', array('all')); } } /** * Returns the Drupal major version number (6, 7, 8 ...) */ function drush_drupal_major_version($drupal_root = NULL) { $major_version = FALSE; if ($version = drush_drupal_version($drupal_root)) { $version_parts = explode('.', $version); if (is_numeric($version_parts[0])) { $major_version = (integer)$version_parts[0]; } } return $major_version; } /** * Log Drupal watchdog() calls. * * A sneaky implementation of hook_watchdog(), for D6/D7. */ function system_watchdog($log_entry) { // Transform non informative severity levels to 'error' for compatibility with _drush_print_log. // Other severity levels are coincident with the ones we use in drush. if (drush_drupal_major_version() >= 6 && $log_entry['severity'] <= 2) { $severity = 'error'; } else { drush_include_engine('drupal', 'environment'); $levels = drush_watchdog_severity_levels(); $severity = $levels[$log_entry['severity']]; } // Format the message. if (is_array($log_entry['variables'])) { $message = strtr($log_entry['message'], $log_entry['variables']); } else { $message = $log_entry['message']; } // decode_entities() only loaded after FULL bootstrap. if (function_exists('decode_entities')) { $message = decode_entities($message); } $message = strip_tags($message); // Log or print or ignore. Just printing saves memory but thats rarely needed. switch (drush_get_option('watchdog', 'log')) { case 'log': drush_log('WD '. $log_entry['type'] . ': ' . $message, $severity); break; case 'print': // Disable in backend mode since it logs output and the goal is to conserve memory. // @see _drush_bootstrap_drush(). if (ob_get_length() === FALSE) { drush_print('WD '. $severity . ' ' . $log_entry['type'] . ': ' . $message); } break; default: // Do nothing. } } /** * Log the return value of Drupal hook_update_n functions. * * This is used during install and update to log the output * of the update process to the logging system. */ function _drush_log_update_sql($ret) { if (count($ret)) { foreach ($ret as $info) { if (is_array($info)) { if (!$info['success']) { drush_set_error('DRUPAL_UPDATE_FAILED', $info['query']); } else { drush_log($info['query'], ($info['success']) ? LogLevel::SUCCESS : LogLevel::ERROR); } } } } } function drush_find_profiles($drupal_root , $key = 'name') { return drush_scan_directory($drupal_root . '/profiles', "/.*\.profile$/", array('.', '..', 'CVS', 'tests'), 0, 2, $key); } /** * Parse Drupal info file format. * * Copied with modifications from includes/common.inc. * * @see drupal_parse_info_file */ function drush_drupal_parse_info_file($filename) { if (!file_exists($filename)) { return array(); } $data = file_get_contents($filename); return _drush_drupal_parse_info_file($data); } /** * Parse the info file. */ function _drush_drupal_parse_info_file($data, $merge_item = NULL) { if (!$data) { return FALSE; } if (preg_match_all(' @^\s* # Start at the beginning of a line, ignoring leading whitespace ((?: [^=;\[\]]| # Key names cannot contain equal signs, semi-colons or square brackets, \[[^\[\]]*\] # unless they are balanced and not nested )+?) \s*=\s* # Key/value pairs are separated by equal signs (ignoring white-space) (?: ("(?:[^"]|(?<=\\\\)")*")| # Double-quoted string, which may contain slash-escaped quotes/slashes (\'(?:[^\']|(?<=\\\\)\')*\')| # Single-quoted string, which may contain slash-escaped quotes/slashes ([^\r\n]*?) # Non-quoted string )\s*$ # Stop at the next end of a line, ignoring trailing whitespace @msx', $data, $matches, PREG_SET_ORDER)) { $info = array(); foreach ($matches as $match) { // Fetch the key and value string. $i = 0; foreach (array('key', 'value1', 'value2', 'value3') as $var) { $$var = isset($match[++$i]) ? $match[$i] : ''; } $value = stripslashes(substr($value1, 1, -1)) . stripslashes(substr($value2, 1, -1)) . $value3; // Parse array syntax. $keys = preg_split('/\]?\[/', rtrim($key, ']')); $last = array_pop($keys); $parent = &$info; // Create nested arrays. foreach ($keys as $key) { if ($key == '') { $key = count($parent); } if (isset($merge_item) && isset($parent[$key]) && !is_array($parent[$key])) { $parent[$key] = array($merge_item => $parent[$key]); } if (!isset($parent[$key]) || !is_array($parent[$key])) { $parent[$key] = array(); } $parent = &$parent[$key]; } // Handle PHP constants. if (defined($value)) { $value = constant($value); } // Insert actual value. if ($last == '') { $last = count($parent); } if (isset($merge_item) && isset($parent[$last]) && is_array($parent[$last])) { $parent[$last][$merge_item] = $value; } else { $parent[$last] = $value; } } return $info; } return FALSE; } /** * Build a cache id to store the install_profile for a given site. */ function drush_cid_install_profile() { return drush_get_cid('install_profile', array(), array(drush_get_context('DRUSH_SELECTED_DRUPAL_SITE_CONF_PATH'))); } /* * An array of options shared by sql-sanitize and sql-sync commands. */ function drupal_sanitize_options() { return array( 'sanitize-password' => array( 'description' => 'The password to assign to all accounts in the sanitization operation, or "no" to keep passwords unchanged.', 'example-value' => 'password', 'value' => 'required', ), 'sanitize-email' => array( 'description' => 'The pattern for test email addresses in the sanitization operation, or "no" to keep email addresses unchanged. May contain replacement patterns %uid, %mail or %name.', 'example-value' => 'user+%uid@localhost', 'value' => 'required', ), ); } $name, '!version' => $version, '!extension' => $extension))); } /** * Provide a version-specific class instance. * * @param $class_name * The name of the class to instantiate. Appends the Drupal * major version number to the end of the class name before instantiation. * @param $constructor_args * An array of arguments to pass to the class constructor. * * Example wrapper class to instantiate a widget, called with the * arguments for the WIDGET_CLASS constructor: * * function drush_WIDGET_CLASS_get_class($widgetName, $widgetStyle) { * retrun drush_get_class('Widget_Class', func_get_args())); * } */ function drush_get_class($class_name, $constructor_args = array(), $variations = array()) { if (empty($variations)) { $variations[] = drush_drupal_major_version(); } $class_names = is_array($class_name) ? $class_name : array($class_name); foreach ($class_names as $class_name) { for ($i=count($variations); $i >= 0; $i--) { $variant_class_name = $class_name . implode('', array_slice($variations, 0, $i)); if (class_exists($variant_class_name)) { $reflectionClass = new ReflectionClass($variant_class_name); return !empty($constructor_args) ? $reflectionClass->newInstanceArgs($constructor_args) : $reflectionClass->newInstanceArgs(); } } } // Something bad happenned. TODO Exception? return drush_set_error('DRUSH_GET_CLASS_ERROR', dt('Unable to load class !class', array('!class' => $class_name))); } /** * Generate an .ini file. used by archive-dump." * * @param array $ini * A two dimensional associative array where top level are sections and * second level are key => value pairs. * * @return string * .ini formatted text. */ function drush_export_ini($ini) { $output = ''; foreach ($ini as $section => $pairs) { if ($section) { $output .= "[$section]\n"; } foreach ($pairs as $k => $v) { if ($v) { $output .= "$k = \"$v\"\n"; } } } return $output; } /** * Generate code friendly to the Drupal .info format from a structured array. * Mostly copied from http://drupalcode.org/viewvc/drupal/contributions/modules/features/features.export.inc. * * @param $info * An array or single value to put in a module's .info file. * * @param boolean $integer_keys * Use integer in keys. * * @param $parents * Array of parent keys (internal use only). * * @return * A code string ready to be written to a module's .info file. */ function drush_export_info($info, $integer_keys = FALSE, $parents = array()) { $output = ''; if (is_array($info)) { foreach ($info as $k => $v) { $child = $parents; $child[] = $k; $output .= drush_export_info($v, $integer_keys, $child); } } else if (!empty($info) && count($parents)) { $line = array_shift($parents); foreach ($parents as $key) { $line .= (!$integer_keys && is_numeric($key)) ? "[]" : "[{$key}]"; } $line .= " = \"{$info}\"\n"; return $line; } return $output; } /** * Convert a csv string, or an array of items which * may contain csv strings, into an array of items. * * @param $args * A simple csv string; e.g. 'a,b,c' * or a simple list of items; e.g. array('a','b','c') * or some combination; e.g. array('a,b','c') or array('a,','b,','c,') * * @returns array * A simple list of items (e.g. array('a','b','c') */ function _convert_csv_to_array($args) { // // Step 1: implode(',',$args) converts from, say, array('a,','b,','c,') to 'a,,b,,c,' // Step 2: explode(',', ...) converts to array('a','','b','','c','') // Step 3: array_filter(...) removes the empty items // Step 4: array_map(...) trims extra whitespace from each item // (handles csv strings with extra whitespace, e.g. 'a, b, c') // return array_map('trim', array_filter(explode(',', is_array($args) ? implode(',',$args) : $args))); } /** * Convert a nested array into a flat array. Thows away * the array keys, returning only the values. * * @param $args * An array that may potentially be nested. * e.g. array('a', array('b', 'c')) * * @returns array * A simple list of items (e.g. array('a','b','c') */ function drush_flatten_array($a) { $result = array(); if (!is_array($a)) { return array($a); } foreach ($a as $value) { $result = array_merge($result, drush_flatten_array($value)); } return $result; } /** * Get the available global options. Used by help command. Command files may * modify this list using hook_drush_help_alter(). * * @param boolean $brief * Return a reduced set of important options. Used by help command. * * @return * An associative array containing the option definition as the key, * and a descriptive array for each of the available options. The array * elements for each item are: * * - short-form: The shortcut form for specifying the key on the commandline. * - propagate: backend invoke will use drush_get_option to propagate this * option, when set, to any other Drush command that is called. * - context: The drush context where the value of this item is cached. Used * by backend invoke to propagate values set in code. * - never-post: If TRUE, backend invoke will never POST this item's value * on STDIN; it will always be sent as a commandline option. * - never-propagate: If TRUE, backend invoke will never pass this item on * to the subcommand being executed. * - local-context-only: Backend invoke will only pass this value on for local calls. * - merge: For options such as $options['shell-aliases'] that consist of an array * of items, make a merged array that contains all of the values specified for * all of the contexts (config files) where the option is defined. The value is stored in * the specified 'context', or in a context named after the option itself if the * context flag is not specified. * IMPORTANT: When the merge flag is used, the option value must be obtained via * drush_get_context('option') rather than drush_get_option('option'). * - merge-pathlist: For options such as --include and --config, make a merged list * of options from all contexts; works like the 'merge' flag, but also handles string * values separated by the PATH_SEPARATOR. * - merge-associative: Like 'merge-pathlist', but key values are preserved. * - propagate-cli-value: Used to tell backend invoke to include the value for * this item as specified on the cli. This can either override 'context' * (e.g., propagate --include from cli value instead of DRUSH_INCLUDE context), * or for an independent global setting (e.g. --user) * - description: The help text for this item. displayed by `drush help`. */ function drush_get_global_options($brief = FALSE) { $options['root'] = array('short-form' => 'r', 'short-has-arg' => TRUE, 'never-post' => TRUE, 'description' => "Drupal root directory to use (default: current directory).", 'example-value' => 'path'); $options['uri'] = array('short-form' => 'l', 'short-has-arg' => TRUE, 'never-post' => TRUE, 'description' => 'URI of the drupal site to use (only needed in multisite environments or when running on an alternate port).', 'example-value' => 'http://example.com:8888'); $options['verbose'] = array('short-form' => 'v', 'context' => 'DRUSH_VERBOSE', 'description' => 'Display extra information about the command.'); $options['debug'] = array('short-form' => 'd', 'context' => 'DRUSH_DEBUG', 'description' => 'Display even more information, including internal messages.'); $options['yes'] = array('short-form' => 'y', 'context' => 'DRUSH_AFFIRMATIVE', 'description' => "Assume 'yes' as answer to all prompts."); $options['no'] = array('short-form' => 'n', 'context' => 'DRUSH_NEGATIVE', 'description' => "Assume 'no' as answer to all prompts."); $options['simulate'] = array('short-form' => 's', 'context' => 'DRUSH_SIMULATE', 'never-propagate' => TRUE, 'description' => "Simulate all relevant actions (don't actually change the system)."); $options['pipe'] = array('short-form' => 'p', 'hidden' => TRUE, 'description' => "Emit a compact representation of the command for scripting."); $options['help'] = array('short-form' => 'h', 'description' => "This help system."); if (!$brief) { $options['version'] = array('description' => "Show drush version."); $options['php'] = array('description' => "The absolute path to your PHP interpreter, if not 'php' in the path.", 'example-value' => '/path/to/file', 'never-propagate' => TRUE); $options['interactive'] = array('short-form' => 'ia', 'description' => "Force interactive mode for commands run on multiple targets (e.g. `drush @site1,@site2 cc --ia`).", 'never-propagate' => TRUE); $options['tty'] = array('hidden' => TRUE, 'description' => "Force allocation of tty for remote commands", 'never-propagate' => TRUE); $options['quiet'] = array('short-form' => 'q', 'description' => 'Suppress non-error messages.'); $options['include'] = array('short-form' => 'i', 'short-has-arg' => TRUE, 'context' => 'DRUSH_INCLUDE', 'never-post' => TRUE, 'propagate-cli-value' => TRUE, 'merge-pathlist' => TRUE, 'description' => "A list of additional directory paths to search for drush commands.", 'example-value' => '/path/dir'); $options['exclude'] = array('propagate-cli-value' => TRUE, 'never-post' => TRUE, 'merge-pathlist' => TRUE, 'description' => "A list of files and directory paths to exclude from consideration when searching for drush commandfiles.", 'example-value' => '/path/dir'); $options['config'] = array('short-form' => 'c', 'short-has-arg' => TRUE, 'context' => 'DRUSH_CONFIG', 'never-post' => TRUE, 'propagate-cli-value' => TRUE, 'merge-pathlist' => TRUE, 'description' => "Specify an additional config file to load. See example.drushrc.php.", 'example-value' => '/path/file'); $options['user'] = array('short-form' => 'u', 'short-has-arg' => TRUE, 'propagate-cli-value' => TRUE, 'description' => "Specify a Drupal user to login with. May be a name or a number.", 'example-value' => 'name_or_number'); $options['backend'] = array('short-form' => 'b', 'never-propagate' => TRUE, 'description' => "Hide all output and return structured data."); $options['choice'] = array('description' => "Provide an answer to a multiple-choice prompt.", 'example-value' => 'number'); $options['variables'] = array('description' => "Comma delimited list of name=value pairs. These values take precedence even over settings.php variable overrides.", 'example-value' => 'foo=bar,baz=yaz'); $options['search-depth'] = array('description' => "Control the depth that drush will search for alias files.", 'example-value' => 'number'); $options['ignored-modules'] = array('description' => "Exclude some modules from consideration when searching for drush command files.", 'example-value' => 'token,views'); $options['no-label'] = array('description' => "Remove the site label that drush includes in multi-site command output (e.g. `drush @site1,@site2 status`)."); $options['label-separator'] = array('description' => "Specify the separator to use in multi-site command output (e.g. `drush @sites pm-list --label-separator=',' --format=csv`)."); $options['nocolor'] = array('context' => 'DRUSH_NOCOLOR', 'propagate-cli-value' => TRUE, 'description' => "Suppress color highlighting on log messages."); $options['show-passwords'] = array('description' => "Show database passwords in commands that display connection information."); $options['show-invoke'] = array('description' => "Show all function names which could have been called for the current command. See drush_invoke()."); $options['watchdog'] = array('description' => "Control logging of Drupal's watchdog() to drush log. Recognized values are 'log', 'print', 'disabled'. Defaults to log. 'print' shows calls to admin but does not add them to the log.", 'example-value' => 'print'); $options['cache-default-class'] = array('description' => "A cache backend class that implements CacheInterface. Defaults to JSONCache.", 'example-value' => 'JSONCache'); $options['cache-class-'] = array('description' => "A cache backend class that implements CacheInterface to use for a specific cache bin.", 'example-value' => 'className'); $options['early'] = array('description' => "Include a file (with relative or full path) and call the drush_early_hook() function (where 'hook' is the filename). The function is called pre-bootstrap and offers an opportunity to alter the drush bootstrap environment or process (returning FALSE from the function will continue the bootstrap), or return output very rapidly (e.g. from caches). See includes/complete.inc for an example."); $options['alias-path'] = array('context' => 'ALIAS_PATH', 'local-context-only' => TRUE, 'merge-pathlist' => TRUE, 'propagate-cli-value' => TRUE, 'description' => "Specifies the list of paths where drush will search for alias files.", 'example-value' => '/path/alias1:/path/alias2'); $options['backup-location'] = array('description' => "Specifies the directory where drush will store backups.", 'example-value' => '/path/to/dir'); $options['confirm-rollback'] = array('description' => 'Wait for confirmation before doing a rollback when something goes wrong.'); $options['complete-debug'] = array('hidden' => TRUE, 'description' => "Turn on debug mode forf completion code"); $options['php-options'] = array('description' => "Options to pass to `php` when running drush. Only effective when using the drush.launcher script.", 'never-propagate' => TRUE, 'example-value' => '-d error_reporting="E_ALL"'); $options['halt-on-error'] = array('propagate-cli-value' => TRUE, 'description' => "Controls error handling of recoverable errors (E_RECOVERABLE_ERROR). --halt-on-error=1: Execution is halted. --halt-on-error=0: Drush execution continues"); $options['deferred-sanitization'] = array('hidden' => TRUE, 'description' => "Defer calculating the sanitization operations until after the database has been copied. This is done automatically if the source database is remote."); $options['remote-host'] = array('hidden' => TRUE, 'description' => 'Remote site to execute drush command on. Managed by site alias.'); $options['remote-user'] = array('hidden' => TRUE, 'description' => 'User account to use with a remote drush command. Managed by site alias.'); $options['remote-os'] = array('hidden' => TRUE, 'description' => 'The operating system used on the remote host. Managed by site alias.'); $options['site-list'] = array('hidden' => TRUE, 'description' => 'List of sites to run commands on. Managed by site alias.'); $options['reserve-margin'] = array('hidden' => TRUE, 'description' => 'Remove columns from formatted opions. Managed by multi-site command handling.'); $options['strict'] = array('propagate' => TRUE, 'description' => 'Return an error on unrecognized options. --strict=0: Allow unrecognized options. --strict=2: Also return an error on any "warning" log messages. Optional. Default is 1.'); $options['command-specific'] = array('hidden' => TRUE, 'merge-associative' => TRUE, 'description' => 'Command-specific options.'); $options['site-aliases'] = array('hidden' => TRUE, 'merge-associative' => TRUE, 'description' => 'List of site aliases.'); $options['shell-aliases'] = array('hidden' => TRUE, 'merge' => TRUE, 'never-propagate' => TRUE, 'description' => 'List of shell aliases.'); $options['path-aliases'] = array('hidden' => TRUE, 'never-propagate' => TRUE, 'description' => 'Path aliases from site alias.'); $options['ssh-options'] = array('never-propagate' => TRUE, 'description' => 'A string of extra options that will be passed to the ssh command', 'example-value' => '-p 100'); $options['editor'] = array('never-propagate' => TRUE, 'description' => 'A string of bash which launches user\'s preferred text editor. Defaults to ${VISUAL-${EDITOR-vi}}.', 'example-value' => 'vi'); $options['bg'] = array('never-propagate' => TRUE, 'description' => 'Run editor in the background. Does not work with editors such as `vi` that run in the terminal. Supresses config-import at the end.'); $options['db-url'] = array('hidden' => TRUE, 'description' => 'A Drupal 6 style database URL. Used by various commands.', 'example-value' => 'mysql://root:pass@127.0.0.1/db'); $options['drush-coverage'] = array('hidden' => TRUE, 'never-post' => TRUE, 'propagate-cli-value' => TRUE, 'description' => 'File to save code coverage data into.'); $options['redirect-port'] = array('hidden' => TRUE, 'never-propagate' => TRUE, 'description' => 'Used by the user-login command to specify the redirect port on the local machine; it therefore would not do to pass this to the remote machines.'); $options['cache-clear'] = array('propagate' => TRUE, 'description' => 'If 0, Drush skips normal cache clearing; the caller should then clear if needed.', 'example-value' => '0', ); $options['local'] = array('propagate' => TRUE, 'description' => 'Don\'t look in global locations for commandfiles, config, and site aliases'); $command = array( 'options' => $options, '#brief' => FALSE, ) + drush_command_defaults('global-options', 'global_options', __FILE__); drush_command_invoke_all_ref('drush_help_alter', $command); $options = $command['options']; } return $options; } /** * Exits with a message. In general, you should use drush_set_error() instead of * this function. That lets drush proceed with other tasks. * TODO: Exit with a correct status code. */ function drush_die($msg = NULL, $status = NULL) { die($msg ? "drush: $msg\n" : ''); } /** * Check to see if the provided line is a "#!/usr/bin/env drush" * "shebang" script line. */ function _drush_is_drush_shebang_line($line) { return ((substr($line,0,2) == '#!') && (strstr($line, 'drush') !== FALSE)); } /** * Check to see if the provided script file is a "#!/usr/bin/env drush" * "shebang" script line. */ function _drush_is_drush_shebang_script($script_filename) { $result = FALSE; if (file_exists($script_filename)) { $fp = fopen($script_filename, "r"); if ($fp !== FALSE) { $line = fgets($fp); $result = _drush_is_drush_shebang_line($line); fclose($fp); } } return $result; } /** * @defgroup userinput Get input from the user. * @{ */ /** * Asks the user a basic yes/no question. * * @param string $msg * The question to ask. * @param int $indent * The number of spaces to indent the message. * * @return bool * TRUE if the user enters "y" or FALSE if "n". */ function drush_confirm($msg, $indent = 0) { drush_print_prompt((string)$msg . " (y/n): ", $indent); // Automatically accept confirmations if the --yes argument was supplied. if (drush_get_context('DRUSH_AFFIRMATIVE')) { drush_print("y"); return TRUE; } // Automatically cancel confirmations if the --no argument was supplied. elseif (drush_get_context('DRUSH_NEGATIVE')) { drush_print("n"); return FALSE; } // See http://drupal.org/node/499758 before changing this. $stdin = fopen("php://stdin","r"); while ($line = fgets($stdin)) { $line = trim($line); if ($line == 'y') { return TRUE; } if ($line == 'n') { return FALSE; } drush_print_prompt((string)$msg . " (y/n): ", $indent); } } /** * Ask the user to select an item from a list. * From a provided associative array, drush_choice will * display all of the questions, numbered from 1 to N, * and return the item the user selected. "0" is always * cancel; entering a blank line is also interpreted * as cancelling. * * @param $options * A list of questions to display to the user. The * KEYS of the array are the result codes to return to the * caller; the VALUES are the messages to display on * each line. Special keys of the form '-- something --' can be * provided as separator between choices groups. Separator keys * don't alter the numbering. * @param $prompt * The message to display to the user prompting for input. * @param $label * Controls the display of each line. Defaults to * '!value', which displays the value of each item * in the $options array to the user. Use '!key' to * display the key instead. In some instances, it may * be useful to display both the key and the value; for * example, if the key is a user id and the value is the * user name, use '!value (uid=!key)'. */ function drush_choice($options, $prompt = 'Enter a number.', $label = '!value', $widths = array()) { drush_print(dt($prompt)); // Preflight so that all rows will be padded out to the same number of columns $array_pad = 0; foreach ($options as $key => $option) { if (is_array($option) && (count($option) > $array_pad)) { $array_pad = count($option); } } $rows[] = array_pad(array('[0]', ':', 'Cancel'), $array_pad + 2, ''); $selection_number = 0; foreach ($options as $key => $option) { if ((substr($key, 0, 3) == '-- ') && (substr($key, -3) == ' --')) { $rows[] = array_pad(array('', '', $option), $array_pad + 2, ''); } else { $selection_number++; $row = array("[$selection_number]", ':'); if (is_array($option)) { $row = array_merge($row, $option); } else { $row[] = dt($label, array('!number' => $selection_number, '!key' => $key, '!value' => $option)); } $rows[] = $row; $selection_list[$selection_number] = $key; } } drush_print_table($rows, FALSE, $widths); drush_print_pipe(array_keys($options)); // If the user specified --choice, then make an // automatic selection. Cancel if the choice is // not an available option. if (($choice = drush_get_option('choice', FALSE)) !== FALSE) { // First check to see if $choice is one of the symbolic options if (array_key_exists($choice, $options)) { return $choice; } // Next handle numeric selections elseif (array_key_exists($choice, $selection_list)) { return $selection_list[$choice]; } return FALSE; } // If the user specified --no, then cancel; also avoid // getting hung up waiting for user input in --pipe and // backend modes. If none of these apply, then wait, // for user input and return the selected result. if (!drush_get_context('DRUSH_NEGATIVE') && !drush_get_context('DRUSH_AFFIRMATIVE') && !drush_get_context('DRUSH_PIPE')) { while ($line = trim(fgets(STDIN))) { if (array_key_exists($line, $selection_list)) { return $selection_list[$line]; } } } // We will allow --yes to confirm input if there is only // one choice; otherwise, --yes will cancel to avoid ambiguity if (drush_get_context('DRUSH_AFFIRMATIVE') && (count($options) == 1)) { return $selection_list[1]; } return FALSE; } /** * Ask the user to select multiple items from a list. * This is a wrapper around drush_choice, that repeats the selection process, * allowing users to toggle a number of items in a list. The number of values * that can be constrained by both min and max: the user will only be allowed * finalize selection once the minimum number has been selected, and the oldest * selected value will "drop off" the list, if they exceed the maximum number. * * @param $options * Same as drush_choice() (see above). * @param $defaults * This can take 3 forms: * - FALSE: (Default) All options are unselected by default. * - TRUE: All options are selected by default. * - Array of $options keys to be selected by default. * @param $prompt * Same as drush_choice() (see above). * @param $label * Same as drush_choice() (see above). * @param $mark * Controls how selected values are marked. Defaults to '!value (selected)'. * @param $min * Constraint on minimum number of selections. Defaults to zero. When fewer * options than this are selected, no final options will be available. * @param $max * Constraint on minimum number of selections. Defaults to NULL (unlimited). * If the a new selection causes this value to be exceeded, the oldest * previously selected value is automatically unselected. * @param $final_options * An array of additional options in the same format as $options. * When the minimum number of selections is met, this array is merged into the * array of options. If the user selects one of these values and the * selection process will complete (the key for the final option is included * in the return value). If this is an empty array (default), then a built in * final option of "Done" will be added to the available options (in this case * no additional keys are added to the return value). */ function drush_choice_multiple($options, $defaults = FALSE, $prompt = 'Select some numbers.', $label = '!value', $mark = '!value (selected)', $min = 0, $max = NULL, $final_options = array()) { $selections = array(); // Load default selections. if (is_array($defaults)) { $selections = $defaults; } elseif ($defaults === TRUE) { $selections = array_keys($options); } $complete = FALSE; $final_builtin = array(); if (empty($final_options)) { $final_builtin['done'] = dt('Done'); } $final_options_keys = array_keys($final_options); while (TRUE) { $current_options = $options; // Mark selections. foreach ($selections as $selection) { $current_options[$selection] = dt($mark, array('!key' => $selection, '!value' => $options[$selection])); } // Add final options, if the minimum number of selections has been reached. if (count($selections) >= $min) { $current_options = array_merge($current_options, $final_options, $final_builtin); } $toggle = drush_choice($current_options, $prompt, $label); if ($toggle === FALSE) { return FALSE; } // Don't include the built in final option in the return value. if (count($selections) >= $min && empty($final_options) && $toggle == 'done') { return $selections; } // Toggle the selected value. $item = array_search($toggle, $selections); if ($item === FALSE) { array_unshift($selections, $toggle); } else { unset($selections[$item]); } // If the user selected one of the final options, return. if (count($selections) >= $min && in_array($toggle, $final_options_keys)) { return $selections; } // If the user selected too many options, drop the oldest selection. if (isset($max) && count($selections) > $max) { array_pop($selections); } } } /** * Prompt the user for input * * The input can be anything that fits on a single line (not only y/n), * so we can't use drush_confirm() * * @param $prompt * The text which is displayed to the user. * @param $default * The default value of the input. * @param $required * If TRUE, user may continue even when no value is in the input. * @param $password * If TRUE, surpress printing of the input. * * @see drush_confirm() */ function drush_prompt($prompt, $default = NULL, $required = TRUE, $password = FALSE) { if (isset($default)) { $prompt .= " [" . $default . "]"; } $prompt .= ": "; drush_print_prompt($prompt); if (drush_get_context('DRUSH_AFFIRMATIVE')) { return $default; } $stdin = fopen('php://stdin', 'r'); if ($password) drush_shell_exec("stty -echo"); stream_set_blocking($stdin, TRUE); while (($line = fgets($stdin)) !== FALSE) { $line = trim($line); if ($line === "") { $line = $default; } if ($line || !$required) { break; } drush_print_prompt($prompt); } fclose($stdin); if ($password) { drush_shell_exec("stty echo"); print "\n"; } return $line; } /** * @} End of "defgroup userinput". */ /** * Calls a given function, passing through all arguments unchanged. * * This should be used when calling possibly mutative or destructive functions * (e.g. unlink() and other file system functions) so that can be suppressed * if the simulation mode is enabled. * * Important: Call @see drush_op_system() to execute a shell command, * or @see drush_shell_exec() to execute a shell command and capture the * shell output. * * @param $callable * The name of the function. Any additional arguments are passed along. * @return * The return value of the function, or TRUE if simulation mode is enabled. * */ function drush_op($callable) { $args_printed = array(); $args = func_get_args(); array_shift($args); // Skip function name foreach ($args as $arg) { $args_printed[] = is_scalar($arg) ? $arg : (is_array($arg) ? 'Array' : 'Object'); } if (!is_array($callable)) { $callable_string = $callable; } else { if (is_object($callable[0])) { $callable_string = get_class($callable[0]) . '::' . $callable[1]; } else { $callable_string = implode('::', $callable); } } // Special checking for drush_op('system') if ($callable == 'system') { drush_log(dt("Do not call drush_op('system'); use drush_op_system instead"), LogLevel::DEBUG); } if (drush_get_context('DRUSH_VERBOSE') || drush_get_context('DRUSH_SIMULATE')) { drush_log(sprintf("Calling %s(%s)", $callable_string, implode(", ", $args_printed)), LogLevel::DEBUG); } if (drush_get_context('DRUSH_SIMULATE')) { return TRUE; } return drush_call_user_func_array($callable, $args); } /** * Mimic cufa but still call function directly. See http://drupal.org/node/329012#comment-1260752 */ function drush_call_user_func_array($function, $args = array() ) { if (is_array($function)) { // $callable is a method so always use CUFA. return call_user_func_array($function, $args); } switch (count($args)) { case 0: return $function(); break; case 1: return $function($args[0]); break; case 2: return $function($args[0], $args[1]); break; case 3: return $function($args[0], $args[1], $args[2]); break; case 4: return $function($args[0], $args[1], $args[2], $args[3]); break; case 5: return $function($args[0], $args[1], $args[2], $args[3], $args[4]); break; case 6: return $function($args[0], $args[1], $args[2], $args[3], $args[4], $args[5]); break; case 7: return $function($args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6]); break; case 8: return $function($args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6], $args[7]); break; case 9: return $function($args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6], $args[7], $args[8]); break; default: return call_user_func_array($function,$args); } } /** * Download a file using wget, curl or file_get_contents, or via download cache. * * @param string $url * The url of the file to download. * @param string $destination * The name of the file to be saved, which may include the full path. * Optional, if omitted the filename will be extracted from the url and the * file downloaded to the current working directory (Drupal root if * bootstrapped). * @param integer $cache_duration * The acceptable age of a cached file. If cached file is too old, a fetch * will occur and cache will be updated. Optional, if ommitted the file will * be fetched directly. * * @return string * The path to the downloaded file, or FALSE if the file could not be * downloaded. */ function drush_download_file($url, $destination = FALSE, $cache_duration = 0) { // Generate destination if omitted. if (!$destination) { $file = basename(current(explode('?', $url, 2))); $destination = getcwd() . '/' . basename($file); } // Simply copy local files to the destination if (!_drush_is_url($url)) { return copy($url, $destination) ? $destination : FALSE; } if ($cache_duration !== 0 && $cache_file = drush_download_file_name($url)) { // Check for cached, unexpired file. if (file_exists($cache_file) && filectime($cache_file) > ($_SERVER['REQUEST_TIME']-$cache_duration)) { drush_log(dt('!name retrieved from cache.', array('!name' => $cache_file))); } else { if (_drush_download_file($url, $cache_file, TRUE)) { // Cache was set just by downloading file to right location. } elseif (file_exists($cache_file)) { drush_log(dt('!name retrieved from an expired cache since refresh failed.', array('!name' => $cache_file)), LogLevel::WARNING); } else { $cache_file = FALSE; } } if ($cache_file && copy($cache_file, $destination)) { // Copy cached file to the destination return $destination; } } elseif ($return = _drush_download_file($url, $destination)) { drush_register_file_for_deletion($return); return $return; } // Unable to retrieve from cache nor download. return FALSE; } /** * Helper function to determine name of cached file. */ function drush_download_file_name($url) { if ($cache_dir = drush_directory_cache('download')) { $cache_name = str_replace(array(':', '/', '?', '=', '\\'), '-', $url); return $cache_dir . "/" . $cache_name; } else { return FALSE; } } /** * Check whether the given path is just a url or a local path * @param string $url * @return boolean * TRUE if the path does not contain a schema:// part. */ function _drush_is_url($url) { return parse_url($url, PHP_URL_SCHEME) !== NULL; } /** * Download a file using wget, curl or file_get_contents. Does not use download * cache. * * @param string $url * The url of the file to download. * @param string $destination * The name of the file to be saved, which may include the full path. * @param boolean $overwrite * Overwrite any file thats already at the destination. * @return string * The path to the downloaded file, or FALSE if the file could not be * downloaded. */ function _drush_download_file($url, $destination, $overwrite = TRUE) { static $use_wget; if ($use_wget === NULL) { $use_wget = drush_shell_exec('wget --version'); } $destination_tmp = drush_tempnam('download_file'); if ($use_wget) { drush_shell_exec("wget -q --timeout=30 -O %s %s", $destination_tmp, $url); } else { // Force TLS1+ as per https://github.com/drush-ops/drush/issues/894. drush_shell_exec("curl --tlsv1 --fail -s -L --connect-timeout 30 -o %s %s", $destination_tmp, $url); } if (!drush_file_not_empty($destination_tmp) && $file = @file_get_contents($url)) { @file_put_contents($destination_tmp, $file); } if (!drush_file_not_empty($destination_tmp)) { // Download failed. return FALSE; } drush_move_dir($destination_tmp, $destination, $overwrite); return $destination; } /** * Determines the MIME content type of the specified file. * * The power of this function depends on whether the PHP installation * has either mime_content_type() or finfo installed -- if not, only tar, * gz, zip and bzip2 types can be detected. * * If mime type can't be obtained, an error will be set. * * @return mixed * The MIME content type of the file or FALSE. */ function drush_mime_content_type($filename) { $content_type = drush_attempt_mime_content_type($filename); if ($content_type) { drush_log(dt('Mime type for !file is !mt', array('!file' => $filename, '!mt' => $content_type)), LogLevel::NOTICE); return $content_type; } return drush_set_error('MIME_CONTENT_TYPE_UNKNOWN', dt('Unable to determine mime type for !file.', array('!file' => $filename))); } /** * Works like drush_mime_content_type, but does not set an error * if the type is unknown. */ function drush_attempt_mime_content_type($filename) { $content_type = FALSE; if (class_exists('finfo')) { $finfo = new finfo(FILEINFO_MIME_TYPE); $content_type = $finfo->file($filename); if ($content_type == 'application/octet-stream') { drush_log(dt('Mime type for !file is application/octet-stream.', array('!file' => $filename)), LogLevel::DEBUG); $content_type = FALSE; } } // If apache is configured in such a way that all files are considered // octet-stream (e.g with mod_mime_magic and an http conf that's serving all // archives as octet-stream for other reasons) we'll detect mime types on our // own by examing the file's magic header bytes. if (!$content_type) { drush_log(dt('Examining !file headers.', array('!file' => $filename)), LogLevel::DEBUG); if ($file = fopen($filename, 'rb')) { $first = fread($file, 2); fclose($file); if ($first !== FALSE) { // Interpret the two bytes as a little endian 16-bit unsigned int. $data = unpack('v', $first); switch ($data[1]) { case 0x8b1f: // First two bytes of gzip files are 0x1f, 0x8b (little-endian). // See http://www.gzip.org/zlib/rfc-gzip.html#header-trailer $content_type = 'application/x-gzip'; break; case 0x4b50: // First two bytes of zip files are 0x50, 0x4b ('PK') (little-endian). // See http://en.wikipedia.org/wiki/Zip_(file_format)#File_headers $content_type = 'application/zip'; break; case 0x5a42: // First two bytes of bzip2 files are 0x5a, 0x42 ('BZ') (big-endian). // See http://en.wikipedia.org/wiki/Bzip2#File_format $content_type = 'application/x-bzip2'; break; default: drush_log(dt('Unable to determine mime type from header bytes 0x!hex of !file.', array('!hex' => dechex($data[1]), '!file' => $filename,), LogLevel::DEBUG)); } } else { drush_log(dt('Unable to read !file.', array('!file' => $filename)), LogLevel::WARNING); } } else { drush_log(dt('Unable to open !file.', array('!file' => $filename)), LogLevel::WARNING); } } // 3. Lastly if above methods didn't work, try to guess the mime type from // the file extension. This is useful if the file has no identificable magic // header bytes (for example tarballs). if (!$content_type) { drush_log(dt('Examining !file extension.', array('!file' => $filename)), LogLevel::DEBUG); // Remove querystring from the filename, if present. $filename = basename(current(explode('?', $filename, 2))); $extension_mimetype = array( '.tar' => 'application/x-tar', '.sql' => 'application/octet-stream', ); foreach ($extension_mimetype as $extension => $ct) { if (substr($filename, -strlen($extension)) === $extension) { $content_type = $ct; break; } } } return $content_type; } /** * Check whether a file is a supported tarball. * * @return mixed * The file content type if it's a tarball. FALSE otherwise. */ function drush_file_is_tarball($path) { $content_type = drush_attempt_mime_content_type($path); $supported = array( 'application/x-bzip2', 'application/x-gzip', 'application/x-tar', 'application/x-zip', 'application/zip', ); if (in_array($content_type, $supported)) { return $content_type; } return FALSE; } /** * Extract a tarball. * * @param string $path * Path to the archive to be extracted. * @param string $destination * The destination directory the tarball should be extracted into. * Optional, if ommitted the tarball directory will be used as destination. * @param boolean $listing * If TRUE, a listing of the tar contents will be returned on success. * @param string $tar_extra_options * Extra options to be passed to the tar command. * * @return mixed * TRUE on success, FALSE on fail. If $listing is TRUE, a file listing of the * tarball is returned if the extraction reported success, instead of TRUE. */ function drush_tarball_extract($path, $destination = FALSE, $listing = FALSE, $tar_extra_options = '') { // Check if tarball is supported. if (!($mimetype = drush_file_is_tarball($path))) { return drush_set_error('TARBALL_EXTRACT_UNKNOWN_FORMAT', dt('Unable to extract !path. Unknown archive format.', array('!path' => $path))); } // Check if destination is valid. if (!$destination) { $destination = dirname($path); } if (!drush_mkdir($destination)) { // drush_mkdir already set an error. return FALSE; } // Perform the extraction of a zip file. if (($mimetype == 'application/zip') || ($mimetype == 'application/x-zip')) { $return = drush_shell_cd_and_exec(dirname($path), "unzip %s -d %s", $path, $destination); if (!$return) { return drush_set_error('DRUSH_TARBALL_EXTRACT_ERROR', dt('Unable to unzip !filename.', array('!filename' => $path))); } if ($listing) { // unzip prefixes the file listing output with a header line, // and prefixes each line with a verb representing the compression type. $output = drush_shell_exec_output(); // Remove the header line. array_shift($output); // Remove the prefix verb from each line. $output = array_map( function ($str) use ($destination) { return substr($str, strpos($str, ":") + 3 + strlen($destination)); }, $output ); // Remove any remaining blank lines. $return = array_filter( $output, function ($str) { return $str != ""; } ); } } // Otherwise we have a possibly-compressed Tar file. // If we are not on Windows, then try to do "tar" in a single operation. elseif (!drush_is_windows()) { $tar = drush_get_tar_executable(); $tar_compression_flag = ''; if ($mimetype == 'application/x-gzip') { $tar_compression_flag = 'z'; } elseif ($mimetype == 'application/x-bzip2') { $tar_compression_flag = 'j'; } $return = drush_shell_cd_and_exec(dirname($path), "$tar {$tar_extra_options} -C %s -x%sf %s", $destination, $tar_compression_flag, basename($path)); if (!$return) { return drush_set_error('DRUSH_TARBALL_EXTRACT_ERROR', dt('Unable to untar !filename.', array('!filename' => $path))); } if ($listing) { // We use a separate tar -tf instead of -xvf above because // the output is not the same in Mac. drush_shell_cd_and_exec(dirname($path), "$tar -t%sf %s", $tar_compression_flag, basename($path)); $return = drush_shell_exec_output(); } } // In windows, do the extraction by its primitive steps. else { // 1. copy the source tarball to the destination directory. Rename to a // temp name in case the destination directory == dirname($path) $tmpfile = drush_tempnam(basename($path), $destination); drush_copy_dir($path, $tmpfile, FILE_EXISTS_OVERWRITE); // 2. uncompress the tarball, if compressed. if (($mimetype == 'application/x-gzip') || ($mimetype == 'application/x-bzip2')) { if ($mimetype == 'application/x-gzip') { $compressed = $tmpfile . '.gz'; // We used to use gzip --decompress in --stdout > out, but the output // redirection sometimes failed on Windows for some binary output. $command = 'gzip --decompress %s'; } elseif ($mimetype == 'application/x-bzip2') { $compressed = $tmpfile . '.bz2'; $command = 'bzip2 --decompress %s'; } drush_op('rename', $tmpfile, $compressed); $return = drush_shell_cd_and_exec(dirname($compressed), $command, $compressed); if (!$return || !file_exists($tmpfile)) { return drush_set_error('DRUSH_TARBALL_EXTRACT_ERROR', dt('Unable to decompress !filename.', array('!filename' => $compressed))); } } // 3. Untar. $tar = drush_get_tar_executable(); $return = drush_shell_cd_and_exec(dirname($tmpfile), "$tar {$tar_extra_options} -xvf %s", basename($tmpfile)); if (!$return) { return drush_set_error('DRUSH_TARBALL_EXTRACT_ERROR', dt('Unable to untar !filename.', array('!filename' => $tmpfile))); } if ($listing) { $return = drush_shell_exec_output(); // Cut off the 'x ' prefix for the each line of the tar output // See http://drupal.org/node/1775520 foreach($return as &$line) { if(strpos($line, "x ") === 0) $line = substr($line, 2); } } // Remove the temporary file so the md5 hash is accurate. unlink($tmpfile); } return $return; } /** * @defgroup commandprocessing Command processing functions. * @{ * * These functions manage command processing by the * main function in drush.php. */ /** * Determine whether or not an argument should be removed from the * DRUSH_COMMAND_ARGS context. This method is used when a Drush * command has set the 'strict-option-handling' flag indicating * that it will pass through all commandline arguments and any * additional options (not known to Drush) to some shell command. * * Take as an example the following call to core-rsync: * * drush --yes core-rsync -v -az --exclude-paths='.git:.svn' local-files/ @site:%files * * In this instance: * * --yes is a global Drush option * * -v is an rsync option. It will make rsync run in verbose mode, * but will not make Drush run in verbose mode due to the fact that * core-rsync sets the 'strict-option-handling' flag. * * --exclude-paths is a local Drush option. It will be converted by * Drush into --exclude='.git' and --exclude='.svn', and then passed * on to the rsync command. * * The parameter $arg passed to this function is one of the elements * of DRUSH_COMMAND_ARGS. It will have values such as: * -v * -az * --exclude-paths='.git:.svn' * local-files/ * @site:%files * * Our job in this function is to determine if $arg should be removed * by virtue of appearing in $removal_list. $removal_list is an array * that will contain values such as 'exclude-paths'. Both the key and * the value of $removal_list is the same. */ function _drush_should_remove_command_arg($arg, $removal_list) { foreach ($removal_list as $candidate) { if (($arg == "-$candidate") || ($arg == "--$candidate") || (substr($arg,0,strlen($candidate)+3) == "--$candidate=") ) { return TRUE; } } return FALSE; } /** * Redispatch the specified command using the same * options that were passed to this invocation of drush. */ function drush_do_command_redispatch($command, $args = array(), $remote_host = NULL, $remote_user = NULL, $drush_path = NULL, $user_interactive = FALSE, $aditional_options = array()) { $additional_global_options = array(); $command_options = drush_redispatch_get_options(); $command_options = $aditional_options + $command_options; if (is_array($command)) { $command_name = $command['command']; // If we are executing a remote command that uses strict option handling, // then mark all of the options in the alias context as global, so that they // will appear before the command name. if (!empty($command['strict-option-handling'])) { foreach(drush_get_context('alias') as $alias_key => $alias_value) { if (array_key_exists($alias_key, $command_options) && !array_key_exists($alias_key, $command['options'])) { $additional_global_options[$alias_key] = $alias_value; } } } } else { $command_name = $command; } // If the path to drush was supplied, then use it to invoke the new command. if ($drush_path == NULL) { $drush_path = drush_get_option('drush-script'); if (!isset($drush_path)) { $drush_folder = drush_get_option('drush'); if (isset($drush)) { $drush_path = $drush_folder . '/drush'; } } } $backend_options = array('drush-script' => $drush_path, 'remote-host' => $remote_host, 'remote-user' => $remote_user, 'integrate' => TRUE, 'additional-global-options' => $additional_global_options); // Set the tty if requested, if the command necessitates it, // or if the user explicitly asks for interactive mode, but // not if interactive mode is forced. tty implies interactive if (drush_get_option('tty') || $user_interactive || !empty($command['remote-tty'])) { $backend_options['#tty'] = TRUE; $backend_options['interactive'] = TRUE; } elseif (drush_get_option('interactive')) { $backend_options['interactive'] = TRUE; } // Run the command in a new process. drush_log(dt('Begin redispatch via drush_invoke_process().')); $values = drush_invoke_process('@self', $command_name, $args, $command_options, $backend_options); drush_log(dt('End redispatch via drush_invoke_process().')); return $values; } /** * @} End of "defgroup commandprocessing". */ /** * @defgroup logging Logging information to be provided as output. * @{ * * These functions are primarily for diagnostic purposes, but also provide an overview of tasks that were taken * by drush. */ /** * Add a log message to the log history. * * This function calls the callback stored in the 'DRUSH_LOG_CALLBACK' context with * the resulting entry at the end of execution. * * This allows you to replace it with custom logging implementations if needed, * such as logging to a file or logging to a database (drupal or otherwise). * * The default callback is the Drush\Log\Logger class with prints the messages * to the shell. * * @param message * String containing the message to be logged. * @param type * The type of message to be logged. Common types are 'warning', 'error', 'success' and 'notice'. * A type of 'ok' or 'success' can also be supplied to flag something that worked. * If you want your log messages to print to screen without the user entering * a -v or --verbose flag, use type 'ok', this prints log messages out to * STDERR, which prints to screen (unless you have redirected it). All other * types of messages will be assumed to be notices. */ function drush_log($message, $type = LogLevel::NOTICE, $error = null) { $entry = array( 'type' => $type, 'message' => $message, 'timestamp' => microtime(TRUE), 'memory' => memory_get_usage(), ); $entry['error'] = $error; return _drush_log($entry); } /** * Future: add some sort of dependency injection to Drush. */ function _drush_create_default_logger() { $drush_logger = new Logger(); drush_set_context('DRUSH_LOGGER', $drush_logger); drush_set_context('DRUSH_LOG_CALLBACK', $drush_logger); } /** * Call the default logger, or the user's log callback, as * appropriate. */ function _drush_log($entry) { $callback = drush_get_context('DRUSH_LOG_CALLBACK'); if ($callback instanceof LoggerInterface) { _drush_log_to_logger($callback, $entry); } elseif ($callback) { $log =& drush_get_context('DRUSH_LOG', array()); $log[] = $entry; drush_backend_packet('log', $entry); return $callback($entry); } } // Maintain compatibility with extensions that hook into // DRUSH_LOG_CALLBACK (e.g. drush_ctex_bonus) function _drush_print_log($entry) { $drush_logger = drush_get_context('DRUSH_LOGGER'); if ($drush_logger) { _drush_log_to_logger($drush_logger, $entry); } } function _drush_log_to_logger($logger, $entry) { $context = $entry; $log_level = $entry['type']; $message = $entry['message']; unset($entry['type']); unset($entry['message']); $logger->log($log_level, $message, $context); } function drush_log_has_errors($types = array(LogLevel::WARNING, LogLevel::ERROR, LogLevel::FAILED)) { $log =& drush_get_context('DRUSH_LOG', array()); foreach ($log as $entry) { if (in_array($entry['type'], $types)) { return TRUE; } } return FALSE; } /** * Backend command callback. Add a log message to the log history. * * @param entry * The log entry. */ function drush_backend_packet_log($entry, $backend_options) { if (!$backend_options['log']) { return; } if (!is_string($entry['message'])) { $entry['message'] = implode("\n", (array)$entry['message']); } $entry['message'] = $entry['message']; if (array_key_exists('#output-label', $backend_options)) { $entry['message'] = $backend_options['#output-label'] . $entry['message']; } // If integrate is FALSE, then log messages are stored in DRUSH_LOG, // but are -not- printed to the console. if ($backend_options['integrate']) { _drush_log($entry); } else { $log =& drush_get_context('DRUSH_LOG', array()); $log[] = $entry; // Yes, this looks odd, but we might in fact be a backend command // that ran another backend command. drush_backend_packet('log', $entry); } } /** * Retrieve the log messages from the log history * * @return * Entire log history */ function drush_get_log() { return drush_get_context('DRUSH_LOG', array()); } /** * Run print_r on a variable and log the output. */ function dlm($object) { drush_log(print_r($object, TRUE)); } /** * Display the pipe output for the current request. */ function drush_pipe_output() { $pipe = drush_get_context('DRUSH_PIPE_BUFFER'); if (!empty($pipe)) { drush_print_r($pipe, NULL, FALSE); } } // Print all timers for the request. function drush_print_timers() { global $timers; $temparray = array(); foreach ((array)$timers as $name => $timerec) { // We have to use timer_read() for active timers, and check the record for others if (isset($timerec['start'])) { $temparray[$name] = timer_read($name); } else { $temparray[$name] = $timerec['time']; } } // Go no farther if there were no timers if (count($temparray) > 0) { // Put the highest cumulative times first arsort($temparray); $table = array(); $table[] = array('Timer', 'Cum (sec)', 'Count', 'Avg (msec)'); foreach ($temparray as $name => $time) { $cum = round($time/1000, 3); $count = $timers[$name]['count']; if ($count > 0) { $avg = round($time/$count, 3); } else { $avg = 'N/A'; } $table[] = array($name, $cum, $count, $avg); } drush_print_table($table, TRUE, array(), STDERR); } } /** * Turn drupal_set_message errors into drush_log errors */ function _drush_log_drupal_messages() { if (function_exists('drupal_get_messages')) { $messages = drupal_get_messages(NULL, TRUE); if (array_key_exists('error', $messages)) { //Drupal message errors. foreach ((array) $messages['error'] as $error) { $error = strip_tags($error); $header = preg_match('/^warning: Cannot modify header information - headers already sent by /i', $error); $session = preg_match('/^warning: session_start\(\): Cannot send session /i', $error); if ($header || $session) { //These are special cases for an unavoidable warnings //that are generated by generating output before Drupal is bootstrapped. //or sending a session cookie (seems to affect d7 only?) //Simply ignore them. continue; } elseif (preg_match('/^warning:/i', $error)) { drush_log(preg_replace('/^warning: /i', '', $error), LogLevel::WARNING); } elseif (preg_match('/^notice:/i', $error)) { drush_log(preg_replace('/^notice: /i', '', $error), LogLevel::NOTICE); } elseif (preg_match('/^user warning:/i', $error)) { // This is a special case. PHP logs sql errors as 'User Warnings', not errors. drush_set_error('DRUSH_DRUPAL_ERROR_MESSAGE', preg_replace('/^user warning: /i', '', $error)); } else { drush_set_error('DRUSH_DRUPAL_ERROR_MESSAGE', $error); } } } unset($messages['error']); // Log non-error messages. foreach ($messages as $type => $items) { foreach ($items as $item) { drush_log(strip_tags($item), $type); } } } } // Copy of format_size() in Drupal. function drush_format_size($size) { if ($size < DRUSH_KILOBYTE) { // format_plural() not always available. return dt('@count bytes', array('@count' => $size)); } else { $size = $size / DRUSH_KILOBYTE; // Convert bytes to kilobytes. $units = array( dt('@size KB', array()), dt('@size MB', array()), dt('@size GB', array()), dt('@size TB', array()), dt('@size PB', array()), dt('@size EB', array()), dt('@size ZB', array()), dt('@size YB', array()), ); foreach ($units as $unit) { if (round($size, 2) >= DRUSH_KILOBYTE) { $size = $size / DRUSH_KILOBYTE; } else { break; } } return str_replace('@size', round($size, 2), $unit); } } /** * @} End of "defgroup logging". */ /** * @defgroup errorhandling Managing errors that occur in the Drush framework. * @{ * Functions that manage the current error status of the Drush framework. * * These functions operate by maintaining a static variable that is a equal to the constant DRUSH_FRAMEWORK_ERROR if an * error has occurred. * This error code is returned at the end of program execution, and provide the shell or calling application with * more information on how to diagnose any problems that may have occurred. */ /** * Set an error code for the error handling system. * * @param \Drupal\Component\Render\MarkupInterface|string $error * A text string identifying the type of error. * @param null|string $message * Optional. Error message to be logged. If no message is specified, * hook_drush_help will be consulted, using a key of 'error:MY_ERROR_STRING'. * @param null|string $output_label * Optional. Label to prepend to the error message. * * @return bool * Always returns FALSE, to allow returning false in the calling functions, * such as return drush_set_error('DRUSH_FRAMEWORK_ERROR'). */ function drush_set_error($error, $message = null, $output_label = "") { $error_code =& drush_get_context('DRUSH_ERROR_CODE', DRUSH_SUCCESS); $error_code = DRUSH_FRAMEWORK_ERROR; $error_log =& drush_get_context('DRUSH_ERROR_LOG', array()); if (is_numeric($error)) { $error = 'DRUSH_FRAMEWORK_ERROR'; } elseif (!is_string($error)) { // Typical case: D8 TranslatableMarkup, implementing MarkupInterface. $error = "$error"; } $message = ($message) ? $message : drush_command_invoke_all('drush_help', 'error:' . $error); if (is_array($message)) { $message = implode("\n", $message); } $error_log[$error][] = $message; if (!drush_backend_packet('set_error', array('error' => $error, 'message' => $message))) { drush_log(($message) ? $output_label . $message : $output_label . $error, LogLevel::ERROR, $error); } return FALSE; } /** * Return the current error handling status * * @return * The current aggregate error status */ function drush_get_error() { return drush_get_context('DRUSH_ERROR_CODE', DRUSH_SUCCESS); } /** * Return the current list of errors that have occurred. * * @return * An associative array of error messages indexed by the type of message. */ function drush_get_error_log() { return drush_get_context('DRUSH_ERROR_LOG', array()); } /** * Check if a specific error status has been set. * * @param error * A text string identifying the error that has occurred. * @return * TRUE if the specified error has been set, FALSE if not */ function drush_cmp_error($error) { $error_log = drush_get_error_log(); if (is_numeric($error)) { $error = 'DRUSH_FRAMEWORK_ERROR'; } return array_key_exists($error, $error_log); } /** * Clear error context. */ function drush_clear_error() { drush_set_context('DRUSH_ERROR_CODE', DRUSH_SUCCESS); } /** * Exit due to user declining a confirmation prompt. * * Usage: return drush_user_abort(); */ function drush_user_abort($msg = NULL) { drush_set_context('DRUSH_USER_ABORT', TRUE); drush_set_context('DRUSH_EXIT_CODE', DRUSH_EXITCODE_USER_ABORT); drush_log($msg ? $msg : dt('Cancelled.'), LogLevel::CANCEL); return FALSE; } /** * Turn PHP error handling off. * * This is commonly used while bootstrapping Drupal for install * or updates. * * This also records the previous error_reporting setting, in * case it wasn't recorded previously. * * @see drush_errors_off() */ function drush_errors_off() { drush_get_context('DRUSH_ERROR_REPORTING', error_reporting(0)); ini_set('display_errors', FALSE); } /** * Turn PHP error handling on. * * We default to error_reporting() here just in * case drush_errors_on() is called before drush_errors_off() and * the context is not yet set. * * @arg $errors string * The default error level to set in drush. This error level will be * carried through further drush_errors_on()/off() calls even if not * provided in later calls. * * @see error_reporting() * @see drush_errors_off() */ function drush_errors_on($errors = null) { if (!isset($errors)) { $errors = error_reporting(); } else { drush_set_context('DRUSH_ERROR_REPORTING', $errors); } error_reporting(drush_get_context('DRUSH_ERROR_REPORTING', $errors)); ini_set('display_errors', TRUE); } /** * @} End of "defgroup errorhandling". */ /** * Get the PHP memory_limit value in bytes. */ function drush_memory_limit() { $value = trim(ini_get('memory_limit')); $last = strtolower($value[strlen($value)-1]); switch ($last) { case 'g': $value *= DRUSH_KILOBYTE; case 'm': $value *= DRUSH_KILOBYTE; case 'k': $value *= DRUSH_KILOBYTE; } return $value; } /** * Unset the named key anywhere in the provided * data structure. */ function drush_unset_recursive(&$data, $unset_key) { if (!empty($data) && is_array($data)) { unset($data[$unset_key]); foreach ($data as $key => $value) { if (is_array($value)) { drush_unset_recursive($data[$key], $unset_key); } } } } /** * Return a list of VCSs reserved files and directories. */ function drush_version_control_reserved_files() { static $files = FALSE; if (!$files) { // Also support VCSs that are not drush vc engines. $files = array('.git', '.gitignore', '.hg', '.hgignore', '.hgrags'); $engine_info = drush_get_engines('version_control'); $vcs = array_keys($engine_info['engines']); foreach ($vcs as $name) { $version_control = drush_include_engine('version_control', $name); $files = array_merge($files, $version_control->reserved_files()); } } return $files; } /** * Generate a random alphanumeric password. Copied from user.module. */ function drush_generate_password($length = 10) { // This variable contains the list of allowable characters for the // password. Note that the number 0 and the letter 'O' have been // removed to avoid confusion between the two. The same is true // of 'I', 1, and 'l'. $allowable_characters = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // Zero-based count of characters in the allowable list: $len = strlen($allowable_characters) - 1; // Declare the password as a blank string. $pass = ''; // Loop the number of times specified by $length. for ($i = 0; $i < $length; $i++) { // Each iteration, pick a random character from the // allowable string and append it to the password: $pass .= $allowable_characters[mt_rand(0, $len)]; } return $pass; } /** * Form an associative array from a linear array. * * This function walks through the provided array and constructs an associative * array out of it. The keys of the resulting array will be the values of the * input array. The values will be the same as the keys unless a function is * specified, in which case the output of the function is used for the values * instead. * * @param $array * A linear array. * @param $function * A name of a function to apply to all values before output. * * @return * An associative array. */ function drush_map_assoc($array, $function = NULL) { // array_combine() fails with empty arrays: // http://bugs.php.net/bug.php?id=34857. $array = !empty($array) ? array_combine($array, $array) : array(); if (is_callable($function)) { $array = array_map($function, $array); } return $array; } /** * Clears completion caches. * * If called with no parameters the entire complete cache will be cleared. * If called with just the $type parameter the global cache for that type will * be cleared (in the site context, if any). If called with both $type and * $command parameters the command cache of that type will be cleared (in the * site context, if any). * * This is included in drush.inc as complete.inc is only loaded conditionally. * * @param $type * The completion type (optional). * @param $command * The command name (optional), if command specific cache is to be cleared. * If specifying a command, $type is not optional. */ function drush_complete_cache_clear($type = NULL, $command = NULL) { require_once DRUSH_BASE_PATH . '/includes/complete.inc'; if ($type) { drush_cache_clear_all(drush_complete_cache_cid($type, $command), 'complete'); return; } // No type or command, so clear the entire complete cache. drush_cache_clear_all('*', 'complete', TRUE); } /* * Cast a value according to the provided format * * @param mixed $value. * @param string $format * Allowed values: auto, integer, float, bool, boolean, json, yaml. * * @return $value * The value, re-typed as needed. */ function drush_value_format($value, $format) { if ($format == 'auto') { if (is_numeric($value)) { $value = $value + 0; // http://php.net/manual/en/function.is-numeric.php#107326 $format = gettype($value); } elseif (($value == 'TRUE') || ($value == 'FALSE')) { $format = 'bool'; } } // Now, we parse the object. switch ($format) { case 'integer': $value = (integer)$value; break; // from: http://php.net/gettype // for historical reasons "double" is returned in case of a float, and not simply "float" case 'double': case 'float': $value = (float)$value; break; case 'bool': case 'boolean': if ($value == 'TRUE') { $value = TRUE; } elseif ($value == 'FALSE') { $value = FALSE; } else { $value = (bool)$value; } break; case 'json': $value = drush_json_decode($value); break; case 'yaml': $value = \Symfony\Component\Yaml\Yaml::parse($value, FALSE, TRUE); break; } return $value; } $data) { $info[$type] += array( 'description' => '', 'option' => FALSE, 'default' => NULL, 'options' => array(), 'sub-options' => array(), 'config-aliases' => array(), 'add-options-to-command' => FALSE, 'combine-help' => FALSE, ); } return $info; } /** * Return a structured array of engines of a specific type. * * Engines are pluggable subsystems. Each engine of a specific type will * implement the same set of API functions and perform the same high-level * task using a different backend or approach. * * This function/hook is useful when you have a selection of several mutually * exclusive options to present to a user to select from. * * Other commands are able to extend this list and provide their own engines. * The hook can return useful information to help users decide which engine * they need, such as description or list of available engine options. * * The engine path element will automatically default to a subdirectory (within * the directory of the commandfile that implemented the hook) with the name of * the type of engine - e.g. an engine "wget" of type "handler" provided by * the "pm" commandfile would automatically be found if the file * "pm/handler/wget.inc" exists and a specific path is not provided. * * @param $engine_type * The type of engine. * * @return * A structured array of engines. */ function drush_get_engines($engine_type) { $info = drush_get_engine_types_info(); if (!isset($info[$engine_type])) { return drush_set_error('DRUSH_UNKNOWN_ENGINE_TYPE', dt('Unknown engine type !engine_type', array('!engine_type' => $engine_type))); } $engines = array( 'info' => $info[$engine_type], 'engines' => array(), ); $list = drush_commandfile_list(); $hook = 'drush_engine_' . str_replace('-', '_', $engine_type); foreach ($list as $commandfile => $path) { if (drush_command_hook($commandfile, $hook)) { $function = $commandfile . '_' . $hook; $result = $function(); foreach ($result as $engine_name => $engine) { // Add some defaults. $engine += array( 'commandfile' => $commandfile, 'options' => array(), 'sub-options' => array(), 'drupal dependencies' => array(), ); // Legacy engines live in a subdirectory // of the commandfile that declared them. $engine_path = sprintf("%s/%s", dirname($path), $engine_type); if (file_exists($engine_path)) { $engine['path'] = $engine_path; } // Build engine class name, in case the engine doesn't provide it. // The class name is based on the engine type and name, converted // from snake_case to CamelCase. // For example for type 'package_handler' and engine 'git_drupalorg' // the class is \Drush\PackageHandler\GitDrupalorg elseif (!isset($engine['class'])) { $parts = array(); $parts[] = '\Drush'; $parts[] = str_replace(' ', '', ucwords(strtr($engine_type, '_', ' '))); $parts[] = str_replace(' ', '', ucwords(strtr($engine_name, '_', ' '))); $engine['class'] = implode('\\', $parts); } $engines['engines'][$engine_name] = $engine; } } } return $engines; } /** * Take a look at all of the available engines, * and create topic commands for each one that * declares a topic. */ function drush_get_engine_topics() { $items = array(); $info = drush_get_engine_types_info(); foreach ($info as $engine => $data) { if (array_key_exists('topic', $data)) { $items[$data['topic']] = array( 'description' => $data['description'], 'hidden' => TRUE, 'topic' => TRUE, 'bootstrap' => DRUSH_BOOTSTRAP_DRUSH, 'callback' => 'drush_engine_topic_command', 'callback arguments' => array($engine), ); } } return $items; } /** * Include, instantiate and validate command engines. * * @return FALSE if a engine doesn't validate. */ function drush_load_command_engines($command) { $result = TRUE; foreach ($command['engines'] as $engine_type => $config) { $result = drush_load_command_engine($command, $engine_type); // Stop loading engines if any of them fails. if ($result === FALSE) { break; } } return $result; } /** * Returns engine config supplied in the command definition. */ function drush_get_command_engine_config($command, $engine_type, $metadata = array()) { if (isset($command['engines'][$engine_type])) { $metadata = array_merge($metadata, $command['engines'][$engine_type]); } return $metadata; } /** * Selects and loads an engine implementing the given type. * * Loaded engines are stored as a context. */ function drush_load_command_engine($command, $engine_type, $metadata = array()) { drush_log(dt("Loading !engine engine.", array('!engine' => $engine_type), LogLevel::BOOTSTRAP)); $config = drush_get_command_engine_config($command, $engine_type, $metadata); $engine_info = drush_get_engines($engine_type); $engine = drush_select_engine($config, $engine_info); $version = drush_drupal_major_version(); $context = $engine_type . '_engine_' . $engine . '_' . $version; $instance = drush_get_context($context, FALSE); if ($instance != FALSE) { drush_set_engine($engine_type, $instance); } else { $instance = drush_load_engine($engine_type, $engine, $config); if ($instance == FALSE) { return FALSE; } drush_set_context($context, $instance); } return $instance; } /** * Add command structure info from each engine type back into the command. */ function drush_merge_engine_data(&$command) { // First remap engine data from the shortcut location // ($command['engine_type']) to the standard location // ($command['engines']['engine_type']) $info = drush_get_engine_types_info(); foreach ($info as $engine_type => $info) { if (isset($command[$engine_type])) { $config = $command[$engine_type]; foreach ($info['config-aliases'] as $engine_option_alias_name => $engine_option_standard_name) { if (array_key_exists($engine_option_alias_name, $config)) { $config[$engine_option_standard_name] = $config[$engine_option_alias_name]; unset($config[$engine_option_alias_name]); } } // Convert single string values of 'require-engine-capability' to an array. if (isset($config['require-engine-capability']) && is_string($config['require-engine-capability'])) { $config['require-engine-capability'] = array($config['require-engine-capability']); } $command['engines'][$engine_type] = $config; } } foreach ($command['engines'] as $engine_type => $config) { // Normalize engines structure. if (!is_array($config)) { unset($command['engines'][$engine_type]); $command['engines'][$config] = array(); $engine_type = $config; } // Get all implementations for this engine type. $engine_info = drush_get_engines($engine_type); if ($engine_info === FALSE) { return FALSE; } // Complete command-declared engine type with default info. $command['engines'][$engine_type] += $engine_info['info']; $config = $command['engines'][$engine_type]; $engine_data = array(); // If there's a single implementation for this engine type, it will be // loaded by default, and makes no sense to provide a command line option // to select the only flavor (ie. --release_info=updatexml), so we won't // add an option in this case. // Additionally, depending on the command, it may be convenient to extend // the command with the engine options. if (count($engine_info['engines']) == 1) { if ($config['add-options-to-command'] !== FALSE) { // Add options and suboptions of the engine type and // the sole implementation. $engine = key($engine_info['engines']); $data = $engine_info['engines'][$engine]; $engine_data += array( 'options' => $config['options'] + $data['options'], 'sub-options' => $config['sub-options'] + $data['sub-options'], ); } } // Otherwise, provide a command option to choose between engines and add // the engine options and sub-options. else { // Add engine type global options and suboptions. $engine_data += array( 'options' => $config['options'], 'sub-options' => $config['sub-options'], ); // If the 'combine-help' flag is set in the engine config, // then we will combine all of the help items into the help // text for $config['option']. $combine_help = $config['combine-help']; $combine_help_data = array(); // Process engines in order. First the default engine, the rest alphabetically. $default = drush_select_engine($config, $engine_info); $engines = array_keys($engine_info['engines']); asort($engines); array_unshift($engines, $default); $engines = array_unique($engines); foreach ($engines as $engine) { $data = $engine_info['engines'][$engine]; // Check to see if the command requires any particular // capabilities. If no capabilities are required, then // all engines are acceptable. $engine_is_usable = TRUE; if (array_key_exists('require-engine-capability', $config)) { // See if the engine declares that it provides any // capabilities. If no capabilities are listed, then // it is assumed that the engine can satisfy all requirements. if (array_key_exists('engine-capabilities', $data)) { $engine_is_usable = FALSE; // If 'require-engine-capability' is TRUE instead of an array, // then only engines that are universal (do not declare any // particular capabilities) are usable. if (is_array($config['require-engine-capability'])) { foreach ($config['require-engine-capability'] as $required) { // We need an engine that provides any one of the requirements. if (in_array($required, $data['engine-capabilities'])) { $engine_is_usable = TRUE; } } } } } if ($engine_is_usable) { $command['engines'][$engine_type]['usable'][] = $engine; if (!isset($data['hidden'])) { $option = $config['option'] . '=' . $engine; $engine_data['options'][$option]['description'] = array_key_exists('description', $data) ? $data['description'] : NULL; if ($combine_help) { $engine_data['options'][$option]['hidden'] = TRUE; if (drush_get_context('DRUSH_VERBOSE') || ($default == $engine) || !isset($data['verbose-only'])) { $combine_help_data[$engine] = $engine . ': ' . $data['description']; } } if (isset($data['options'])) { $engine_data['sub-options'][$option] = $data['options']; } if (isset($data['sub-options'])) { $engine_data['sub-options'] += $data['sub-options']; } } } } if (!empty($combine_help_data)) { $engine_selection_option = $config['option']; if (!is_array($engine_data['options'][$engine_selection_option])) { $engine_data['options'][$engine_selection_option] = array('description' => $config['options'][$engine_selection_option]); } if (drush_get_context('DRUSH_VERBOSE')) { $engine_data['options'][$engine_selection_option]['description'] .= "\n" . dt("All available values are:") . "\n - " . implode("\n - ", $combine_help_data) . "\n"; } else { $engine_data['options'][$engine_selection_option]['description'] .= " " . dt("Available: ") . implode(', ', array_keys($combine_help_data)) . ". "; } $engine_data['options'][$engine_selection_option]['description'] .= dt("Default is !default.", array('!default' => $default)); } else { // If the help options are not combined, then extend the // default engine description. $desc = $engine_info['engines'][$default]['description']; $engine_info['engines'][$default]['description'] = dt('Default !type engine.', array('!type' => $engine_type)) . ' ' . $desc; } } // This was simply array_merge_recursive($command, $engine_data), but this // function has an undesirable behavior when merging primative types. // If there is a 'key' => 'value' in $command, and the same 'key' => 'value' // exists in $engine data, then the result will be 'key' => array('value', 'value');. // This is NOT what we want, so we provide our own 'overlay' function instead. $command = _drush_array_overlay_recursive($command, $engine_data); } } // Works like array_merge_recursive(), but does not convert primative // types into arrays. Ever. function _drush_array_overlay_recursive($a, $b) { foreach ($b as $key => $value) { if (!isset($a[$key]) || !is_array($a[$key])) { $a[$key] = $b[$key]; } else { $a[$key] = _drush_array_overlay_recursive($a[$key], $b[$key]); } } return $a; } /** * Implementation of command hook for docs-output-formats */ function drush_engine_topic_command($engine) { $engine_instances = drush_get_engines($engine); $option = $engine_instances['info']['option']; if (isset($engine_instances['info']['topic-file'])) { // To do: put this file next to the commandfile that defines the // engine type, not in the core docs directory. $docs_dir = drush_get_context('DOC_PREFIX', DRUSH_BASE_PATH); $path = $engine_instances['info']['topic-file']; $docs_file = (drush_is_absolute_path($path) ? '' : $docs_dir . '/') . $path; $doc_text = drush_html_to_text(file_get_contents($docs_file)); } elseif (isset($engine_instances['info']['topic-text'])) { $doc_text = $engine_instances['info']['topic-text']; } else { return drush_set_error('DRUSH_BAD_ENGINE_TOPIC', dt("The engine !engine did not define its topic command correctly.", array('!engine' => $engine))); } // Look at each instance of the engine; if it has an html // file in the the 'topics' folder named after itself, then // include the file contents in the engine topic text. $instances = $engine_instances['engines']; ksort($instances); foreach ($instances as $instance => $config) { if (isset($config['description'])) { $doc_text .= "\n\n::: --$option=$instance :::\n" . $config['description']; $path = $config['path'] . '/topics/' . $instance . '.html'; if (file_exists($path)) { $doc_text .= "\n" . drush_html_to_text(file_get_contents($path)); } $additional_topic_text = drush_command_invoke_all('drush_engine_topic_additional_text', $engine, $instance, $config); if (!empty($additional_topic_text)) { $doc_text .= "\n\n" . implode("\n\n", $additional_topic_text); } } } // Write the topic text to a file so that is can be paged $file = drush_save_data_to_temp_file($doc_text); drush_print_file($file); } /** * Selects an engine between the available ones. * * Precedence: * * - preferred engine, if available. * - user supplied engine via cli. * - default engine from engine type / command declaration. * - the first engine available. * * @param array $config * Engine type configuration. My be overridden in command declaration. * @param array $engine_info * Engine type declaration. * @param string $default * Preferred engine. * * @return string * Selected engine. */ function drush_select_engine($config, $engine_info, $preferred = NULL) { $engines = array_keys($engine_info['engines']); if (in_array($preferred, $engines)) { return $preferred; } if (!empty($config['option'])) { $engine = drush_get_option($config['option'], FALSE); if ($engine && in_array($engine, $engines)) { return $engine; } } if (isset($config['default']) && in_array($config['default'], $engines)) { return $config['default']; } return current($engines); } /** * Loads and validate an engine of the given type. * * @param string $type * Engine type. * @param string $engine * Engine name. * @param array $config * Engine configuration. Tipically it comes from a command declaration. * * @return * TRUE or instanced object of available class on success. FALSE on fail. */ function drush_load_engine($type, $engine, $config = array()) { $engine_info = drush_get_engines($type); $engine = drush_select_engine($config, $engine_info, $engine); $config['engine-info'] = $engine_info['engines'][$engine]; // Check engine dependency on drupal modules before include. $dependencies = $config['engine-info']['drupal dependencies']; foreach ($dependencies as $dependency) { if (!drush_module_exists($dependency)) { return drush_set_error('DRUSH_ENGINE_DEPENDENCY_ERROR', dt('!engine_type: !engine engine needs the following modules installed/enabled to run: !dependencies.', array('!engine_type' => $type, '!engine' => $engine, '!dependencies' => implode(', ', $dependencies)))); } } $result = drush_include_engine($type, $engine, $config); if (is_object($result)) { $valid = method_exists($result, 'validate') ? $result->validate() : TRUE; if ($valid) { drush_set_engine($type, $result); } } else { $function = strtr($type, '-', '_') . '_validate'; $valid = function_exists($function) ? call_user_func($function) : TRUE; } if (!$valid) { return FALSE; } return $result; } /** * Include the engine code for a specific named engine of a certain type. * * If the engine type has implemented hook_drush_engine_$type the path to the * engine specified in the array will be used. * * If a class named in the form drush_$type_$engine exists, it will return an * instance of the class. * * @param string $type * The type of engine. * @param string $engine * The name for the engine to include. * @param array $config * Parameters for the engine class constructor. * * @return * TRUE or instanced object of available class on success. FALSE on fail. */ function drush_include_engine($type, $engine, $config = NULL) { $engine_info = drush_get_engines($type); // Pick the engine name that actually implements the requested engine. $engine = isset($engine_info['engines'][$engine]['implemented-by']) ? $engine_info['engines'][$engine]['implemented-by'] : $engine; // Legacy engines live in a subdirectory of the commandfile // that declares them. We need to explicitly include the file. if (isset($engine_info['engines'][$engine]['path'])) { $path = $engine_info['engines'][$engine]['path']; if (!drush_include($path, $engine)) { return drush_set_error('DRUSH_ENGINE_INCLUDE_FAILED', dt('Unable to include the !type engine !engine from !path.' , array('!path' => $path, '!type' => $type, '!engine' => $engine))); } // Legacy engines may be implemented in a magic class name. $class = 'drush_' . $type . '_' . str_replace('-', '_', $engine); if (class_exists($class)) { $instance = new $class($config); $instance->engine_type = $type; $instance->engine = $engine; return $instance; } return TRUE; } return drush_get_class($engine_info['engines'][$engine]['class'], array($type, $engine, $config)); } /** * Return the engine of the specified type that was loaded by the Drush command. */ function drush_get_engine($type) { return drush_get_context($type . '_engine', FALSE); } /** * Called by the Drush command (@see _drush_load_command_engines()) * to cache the active engine instance. */ function drush_set_engine($type, $instance) { drush_set_context($type . '_engine', $instance); } /** * Add engine topics to the command topics, if any. */ function drush_engine_add_help_topics(&$command) { $engine_types = drush_get_engine_types_info(); foreach ($command['engines'] as $engine_type => $config) { $info = $engine_types[$engine_type]; if (isset($info['topics'])) { $command['topics'] = array_merge($command['topics'], $info['topics']); } if (isset($info['topic'])) { $command['topics'][] = $info['topic']; } } } php_ini_loaded_file())); } else { return dt('Please check your configuration settings in your php.ini file or in your drush.ini file; see examples/example.drush.ini for details.'); } } /** * Evalute the environment after an abnormal termination and * see if we can determine any configuration settings that the user might * want to adjust. */ function _drush_postmortem() { // Make sure that the memory limit has been bumped up from the minimum default value of 32M. $php_memory_limit = drush_memory_limit(); if (($php_memory_limit > 0) && ($php_memory_limit <= 32*DRUSH_KILOBYTE*DRUSH_KILOBYTE)) { drush_set_error('DRUSH_MEMORY_LIMIT', dt('Your memory limit is set to !memory_limit; Drush needs as much memory to run as Drupal. !php_ini_msg', array('!memory_limit' => $php_memory_limit / (DRUSH_KILOBYTE*DRUSH_KILOBYTE) . 'M', '!php_ini_msg' => _drush_php_ini_loaded_file_message()))); } } /** * Evaluate the environment before command bootstrapping * begins. If the php environment is too restrictive, then * notify the user that a setting change is needed and abort. */ function _drush_environment_check_php_ini() { $ini_checks = array('safe_mode' => '', 'open_basedir' => '', 'disable_functions' => array('exec', 'system'), 'disable_classes' => ''); // Test to insure that certain php ini restrictions have not been enabled $prohibited_list = array(); foreach ($ini_checks as $prohibited_mode => $disallowed_value) { $ini_value = ini_get($prohibited_mode); $invalid_value = FALSE; if (empty($disallowed_value)) { $invalid_value = !empty($ini_value) && (strcasecmp($ini_value, 'off') != 0); } else { foreach ($disallowed_value as $test_value) { if (preg_match('/(^|,)' . $test_value . '(,|$)/', $ini_value)) { $invalid_value = TRUE; } } } if ($invalid_value) { $prohibited_list[] = $prohibited_mode; } } if (!empty($prohibited_list)) { drush_log(dt('The following restricted PHP modes have non-empty values: !prohibited_list. This configuration is incompatible with drush. !php_ini_msg', array('!prohibited_list' => implode(' and ', $prohibited_list), '!php_ini_msg' => _drush_php_ini_loaded_file_message())), LogLevel::ERROR); } return TRUE; } /** * Returns the current working directory. * * This is the directory as it was when drush was started, not the * directory we are currently in. For that, use getcwd() directly. */ function drush_cwd() { if ($path = drush_get_context('DRUSH_OLDCWD')) { return $path; } // We use PWD if available because getcwd() resolves symlinks, which // could take us outside of the Drupal root, making it impossible to find. // $_SERVER['PWD'] isn't set on windows and generates a Notice. $path = isset($_SERVER['PWD']) ? $_SERVER['PWD'] : ''; if (empty($path)) { $path = getcwd(); } // Convert windows paths. $path = Path::canonicalize($path); // Save original working dir case some command wants it. drush_set_context('DRUSH_OLDCWD', $path); return $path; } /** * Converts a Windows path (dir1\dir2\dir3) into a Unix path (dir1/dir2/dir3). * Also converts a cygwin "drive emulation" path (/cygdrive/c/dir1) into a * proper drive path, still with Unix slashes (c:/dir1). */ function _drush_convert_path($path) { $path = str_replace('\\','/', $path); if (drush_is_windows(_drush_get_os()) && !drush_is_cygwin(_drush_get_os())) { $path = preg_replace('/^\/cygdrive\/([A-Za-z])(.*)$/', '\1:\2', $path); } return $path; } /** * Returns parent directory. * * @param string * Path to start from. * * @return string * Parent path of given path. */ function _drush_shift_path_up($path) { if (empty($path)) { return FALSE; } $path = explode(DIRECTORY_SEPARATOR, $path); // Move one directory up. array_pop($path); return implode(DIRECTORY_SEPARATOR, $path); // return dirname($path); } /** * Like Drupal conf_path, but searching from beneath. * Allows proper site uri detection in site sub-directories. * * Essentially looks for a settings.php file. Drush uses this * function to find a usable site based on the user's current * working directory. * * @param string * Search starting path. Defaults to current working directory. * * @return * Current site path (folder containing settings.php) or FALSE if not found. */ function drush_site_path($path = NULL) { $site_path = FALSE; $path = empty($path) ? drush_cwd() : $path; // Check the current path. if (file_exists($path . '/settings.php')) { $site_path = $path; } else { // Move up dir by dir and check each. // Stop if we get to a Drupal root. We don't care // if it is DRUSH_SELECTED_DRUPAL_ROOT or some other root. while (($path = _drush_shift_path_up($path)) && !drush_valid_root($path)) { if (file_exists($path . '/settings.php')) { $site_path = $path; break; } } } $site_root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); if (file_exists($site_root . '/sites/sites.php')) { $sites = array(); // This will overwrite $sites with the desired mappings. include($site_root . '/sites/sites.php'); // We do a reverse lookup here to determine the URL given the site key. if ($match = array_search($site_path, $sites)) { $site_path = $match; } } // Last resort: try from site root if (!$site_path) { if ($site_root) { if (file_exists($site_root . '/sites/default/settings.php')) { $site_path = $site_root . '/sites/default'; } } } return $site_path; } /** * Lookup a site's directory via the sites.php file given a hostname. * * @param $hostname * The hostname of a site. May be converted from URI. * * @return $dir * The directory associated with that hostname or FALSE if not found. */ function drush_site_dir_lookup_from_hostname($hostname, $site_root = NULL) { if (!isset($site_root)) { $site_root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); } if (!empty($site_root) && file_exists($site_root . '/sites/sites.php')) { $sites = array(); // This will overwrite $sites with the desired mappings. include($site_root . '/sites/sites.php'); return isset($sites[$hostname]) ? $sites[$hostname] : FALSE; } else { return FALSE; } } /** * This is a copy of Drupal's conf_path function, taken from D7 and * adjusted slightly to search from the selected Drupal Root. * * Drush uses this routine to find a usable site based on a URI * passed in via a site alias record or the --uri commandline option. * * Drush uses Drupal itself (specifically, the Drupal conf_path function) * to bootstrap the site itself. If the implementation of conf_path * changes, the site should still bootstrap correctly; the only consequence * of this routine not working is that drush configuration files * (drushrc.php) stored with the site's drush folders might not be found. */ function drush_conf_path($server_uri, $require_settings = TRUE) { $drupal_root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); if(empty($drupal_root) || empty($server_uri)) { return NULL; } $parsed_uri = parse_url($server_uri); if (is_array($parsed_uri) && !array_key_exists('scheme', $parsed_uri)) { $parsed_uri = parse_url('http://' . $server_uri); } if (!is_array($parsed_uri)) { return NULL; } $server_host = $parsed_uri['host']; if (array_key_exists('path', $parsed_uri)) { $server_uri = $parsed_uri['path'] . '/index.php'; } else { $server_uri = "/index.php"; } $confdir = 'sites'; $sites = array(); if (file_exists($drupal_root . '/' . $confdir . '/sites.php')) { // This will overwrite $sites with the desired mappings. include($drupal_root . '/' . $confdir . '/sites.php'); } $uri = explode('/', $server_uri); $server = explode('.', implode('.', array_reverse(explode(':', rtrim($server_host, '.'))))); for ($i = count($uri) - 1; $i > 0; $i--) { for ($j = count($server); $j > 0; $j--) { $dir = implode('.', array_slice($server, -$j)) . implode('.', array_slice($uri, 0, $i)); if (isset($sites[$dir]) && file_exists($drupal_root . '/' . $confdir . '/' . $sites[$dir])) { $dir = $sites[$dir]; } if (file_exists($drupal_root . '/' . $confdir . '/' . $dir . '/settings.php') || (!$require_settings && file_exists(DRUPAL_ROOT . '/' . $confdir . '/' . $dir))) { $conf = "$confdir/$dir"; return $conf; } } } $conf = "$confdir/default"; return $conf; } /** * Exhaustive depth-first search to try and locate the Drupal root directory. * This makes it possible to run Drush from a subdirectory of the drupal root. * * @param * Search start path. Defaults to current working directory. * @return * A path to drupal root, or FALSE if not found. */ function drush_locate_root($start_path = NULL) { $drupal_root = FALSE; $start_path = empty($start_path) ? drush_cwd() : $start_path; foreach (array(TRUE, FALSE) as $follow_symlinks) { $path = $start_path; if ($follow_symlinks && is_link($path)) { $path = realpath($path); } // Check the start path. if (drush_valid_root($path)) { $drupal_root = $path; break; } else { // Move up dir by dir and check each. while ($path = _drush_shift_path_up($path)) { if ($follow_symlinks && is_link($path)) { $path = realpath($path); } if (drush_valid_root($path)) { $drupal_root = $path; break 2; } } } } return $drupal_root; } /** * Checks whether given path qualifies as a Drupal root. * * @param string * Path to check. * * @return string * The relative path to common.inc (varies by Drupal version), or FALSE if not * a Drupal root. */ function drush_valid_root($path) { $bootstrap_class = drush_bootstrap_class_for_root($path); return $bootstrap_class != NULL; } /** * Tests the currently loaded database credentials to ensure a database connection can be made. * * @param bool $log_errors * (optional) If TRUE, log error conditions; otherwise be quiet. * * @return bool * TRUE if database credentials are valid. */ function drush_valid_db_credentials() { try { $sql = drush_sql_get_class(); if (!$sqlVersion = drush_sql_get_version()) { drush_log(dt('While checking DB credentials, could not instantiate SQLVersion class.'), 'debug'); return FALSE; } if (!$sqlVersion->valid_credentials($sql->db_spec())) { drush_log(dt('DB credentials are invalid.'), 'debug'); return FALSE; } return $sql->query('SELECT 1;'); } catch (Exception $e) { drush_log(dt('Checking DB credentials yielded error: @e', array('@e' => $e->getMessage())), 'debug'); return FALSE; } } /** * Determine a proper way to call drush again * * This check if we were called directly or as an argument to some * wrapper command (php and sudo are checked now). * * Calling ./drush.php directly yields the following environment: * * _SERVER["argv"][0] => ./drush.php * * Calling php ./drush.php also yields the following: * * _SERVER["argv"][0] => ./drush.php * * Note that the $_ global is defined only in bash and therefore cannot * be relied upon. * * The DRUSH_COMMAND constant is initialised to the value of this * function when environment.inc is loaded. * * @see DRUSH_COMMAND */ function drush_find_drush() { if ($drush = realpath($_SERVER['argv']['0'])) { return Path::canonicalize($drush); } return FALSE; } /** * Verify that we are running PHP through the command line interface. * * This function is useful for making sure that code cannot be run via the web server, * such as a function that needs to write files to which the web server should not have * access to. * * @return * A boolean value that is true when PHP is being run through the command line, * and false if being run through cgi or mod_php. */ function drush_verify_cli() { return (php_sapi_name() == 'cli' || (is_numeric($_SERVER['argc']) && $_SERVER['argc'] > 0)); } /** * Build a drush command suitable for use for Drush to call itself * e.g. in backend_invoke. */ function drush_build_drush_command($drush_path = NULL, $php = NULL, $os = NULL, $remote_command = FALSE, $environment_variables = array()) { $os = _drush_get_os($os); $additional_options = ''; $prefix = ''; if (!$drush_path) { if (!$remote_command) { $drush_path = DRUSH_COMMAND; } else { $drush_path = 'drush'; // drush_is_windows($os) ? 'drush.bat' : 'drush'; } } // If the path to drush points to drush.php, then we will need to // run it via php rather than direct execution. By default, we // will use 'php' unless something more specific was passed in // via the --php flag. if (substr($drush_path, -4) == ".php") { if (!isset($php)) { $php = drush_get_option('php'); if (!isset($php)) { $php = 'php'; } } if (isset($php) && ($php != "php")) { $additional_options .= ' --php=' . drush_escapeshellarg($php, $os); } // We will also add in the php options from --php-options $prefix .= drush_escapeshellarg($php, $os); $php_options = implode(' ', drush_get_context_options('php-options')); if (!empty($php_options)) { $prefix .= ' ' . $php_options; $additional_options .= ' --php-options=' . drush_escapeshellarg($php_options, $os); } } else { // Set environment variables to propogate config to redispatched calls. if (drush_has_bash($os)) { if ($php) { $environment_variables['DRUSH_PHP'] = $php; } if ($php_options_alias = drush_get_option('php-options', NULL, 'alias')) { $environment_variables['PHP_OPTIONS'] = $php_options_alias; } $columns = drush_get_context('DRUSH_COLUMNS'); if (($columns) && ($columns != 80)) { $environment_variables['COLUMNS'] = $columns; } } } // Add environmental variables, if present if (!empty($environment_variables)) { $prefix .= ' env'; foreach ($environment_variables as $key=>$value) { $prefix .= ' ' . drush_escapeshellarg($key, $os) . '=' . drush_escapeshellarg($value, $os); } } return trim($prefix . ' ' . drush_escapeshellarg($drush_path, $os) . $additional_options); } /** * Check if the operating system is Winodws * running some variant of cygwin -- either * Cygwin or the MSYSGIT shell. If you care * which is which, test mingw first. */ function drush_is_cygwin($os = NULL) { return _drush_test_os($os, array("CYGWIN","CWRSYNC","MINGW")); } function drush_is_mingw($os = NULL) { return _drush_test_os($os, array("MINGW")); } /** * Return tar executable name specific for the current OS */ function drush_get_tar_executable() { return drush_is_windows() ? (drush_is_mingw() ? "tar.exe" : "bsdtar.exe") : "tar"; } /** * Check if the operating system is OS X. * This will return TRUE for Mac OS X (Darwin). */ function drush_is_osx($os = NULL) { return _drush_test_os($os, array("DARWIN")); } /** * Checks if the operating system has bash. * * MinGW has bash, but PHP isn't part of MinGW and hence doesn't run in bash. */ function drush_has_bash($os = NULL) { return (drush_is_cygwin($os) && !drush_is_mingw($os)) || !drush_is_windows($os); } /** * Checks operating system and returns * supported bit bucket folder. */ function drush_bit_bucket() { if (drush_has_bash()) { return '/dev/null'; } else { return 'nul'; } } /** * Return the OS we are running under. * * @return string * Linux * WIN* (e.g. WINNT) * CYGWIN * MINGW* (e.g. MINGW32) */ function _drush_get_os($os = NULL) { // The special os "CWRSYNC" can be used to indicate that we are testing // a path that will be passed as an argument to cwRsync, which requires // that the path be converted to /cygdrive/c/path, even on DOS or Powershell. // The special os "RSYNC" can be used to indicate that we want to assume // "CWRSYNC" when cwrsync is installed, or default to the local OS otherwise. if (strtoupper($os) == "RSYNC") { $os = _drush_get_os("LOCAL"); // For now we assume that cwrsync is always installed on Windows, and never installed son any other platform. return drush_is_windows($os) ? "CWRSYNC" : $os; } // We allow "LOCAL" to document, in instances where some parameters are being escaped // for use on a remote machine, that one particular parameter will always be used on // the local machine (c.f. drush_backend_invoke). if (isset($os) && ($os != "LOCAL")) { return $os; } if (_drush_test_os(getenv("MSYSTEM"), array("MINGW"))) { return getenv("MSYSTEM"); } // QUESTION: Can we differentiate between DOS and POWERSHELL? They appear to have the same environment. // At the moment, it does not seem to matter; they behave the same from PHP. // At this point we will just return PHP_OS. return PHP_OS; } function _drush_test_os($os, $os_list_to_check) { $os = _drush_get_os($os); foreach ($os_list_to_check as $test) { if (strtoupper(substr($os, 0, strlen($test))) == strtoupper($test)) { return TRUE; } } return FALSE; } /** * Read the drush info file. */ function drush_read_drush_info() { $drush_info_file = dirname(__FILE__) . '/../drush.info'; return parse_ini_file($drush_info_file); } /** * Make a determination whether or not the given * host is local or not. * * @param host * A hostname, 'localhost' or '127.0.0.1'. * @return * True if the host is local. */ function drush_is_local_host($host) { // Check to see if the provided host is "local". // @see hook_drush_sitealias_alter() in drush.api.php. if ( ($host == 'localhost') || ($host == '127.0.0.1') ) { return TRUE; } return FALSE; } /** * Return the user's home directory. */ function drush_server_home() { try { return Path::getHomeDirectory(); } catch (Exception $e) { return NULL; } } /** * Return the name of the user running drush. */ function drush_get_username() { $name = NULL; if (!$name = getenv("username")) { // Windows if (!$name = getenv("USER")) { // If USER not defined, use posix if (function_exists('posix_getpwuid')) { $processUser = posix_getpwuid(posix_geteuid()); $name = $processUser['name']; } } } return $name; } /** * The path to the global cache directory. * * @param subdir * Return the specified subdirectory inside the global * cache directory instead. The subdirectory is created. */ function drush_directory_cache($subdir = '') { $cache_locations = array(); if (getenv('CACHE_PREFIX')) { $cache_locations[getenv('CACHE_PREFIX')] = 'cache'; } $home = drush_server_home(); if ($home) { $cache_locations[$home] = '.drush/cache'; } $cache_locations[drush_find_tmp()] = 'drush-' . drush_get_username() . '/cache'; foreach ($cache_locations as $base => $dir) { if (!empty($base) && is_writable($base)) { $cache_dir = $base . '/' . $dir; if (!empty($subdir)) { $cache_dir .= '/' . $subdir; } if (drush_mkdir($cache_dir)) { return $cache_dir; } else { // If the base directory is writable, but the cache directory // is not, then we will get an error. The error will be displayed, // but we will still call drush_clear_error so that we can go // on and try the next location to see if we can find a cache // directory somewhere. drush_clear_error(); } } } return drush_set_error('DRUSH_NO_WRITABLE_CACHE', dt('Drush must have a writable cache directory; please insure that one of the following locations is writable: @locations', array('@locations' => implode(',', array_keys($cache_locations))))); } /** * Get complete information for all available extensions (modules and themes). * * @return * An array containing info for all available extensions. In D8, these are Extension objects. */ function drush_get_extensions($include_hidden = TRUE) { drush_include_engine('drupal', 'environment'); $extensions = array_merge(drush_get_modules($include_hidden), drush_get_themes($include_hidden)); foreach ($extensions as $name => $extension) { if (isset($extension->info['name'])) { $extensions[$name]->label = $extension->info['name'].' ('.$name.')'; } else { drush_log(dt("Extension !name provides no human-readable name in .info file.", array('!name' => $name), LogLevel::DEBUG)); $extensions[$name]->label = $name.' ('.$name.')'; } if (empty($extension->info['package'])) { $extensions[$name]->info['package'] = dt('Other'); } } return $extensions; } /** * Gets the extension name. * * @param $info * The extension info. * @return string * The extension name. */ function drush_extension_get_name($info) { drush_include_engine('drupal', 'environment'); return _drush_extension_get_name($info); } /** * Gets the extension type. * * @param $info * The extension info. * @return string * The extension type. */ function drush_extension_get_type($info) { drush_include_engine('drupal', 'environment'); return _drush_extension_get_type($info); } /** * Gets the extension path. * * @param $info * The extension info. * @return string * The extension path. */ function drush_extension_get_path($info) { drush_include_engine('drupal', 'environment'); return _drush_extension_get_path($info); } /** * Test compatibility of a extension with version of drupal core and php. * * @param $file Extension object as returned by system_rebuild_module_data(). * @return FALSE if the extension is compatible. */ function drush_extension_check_incompatibility($file) { if (!isset($file->info['core']) || $file->info['core'] != drush_get_drupal_core_compatibility()) { return 'Drupal'; } if (version_compare(phpversion(), $file->info['php']) < 0) { return 'PHP'; } return FALSE; } /** * */ function drush_drupal_required_modules($modules) { drush_include_engine('drupal', 'environment'); return _drush_drupal_required_modules($modules); } /** * Return the default theme. * * @return * Machine name of the default theme. */ function drush_theme_get_default() { drush_include_engine('drupal', 'environment'); return _drush_theme_default(); } /** * Return the administration theme. * * @return * Machine name of the administration theme. */ function drush_theme_get_admin() { drush_include_engine('drupal', 'environment'); return _drush_theme_admin(); } /** * Return the path to public files directory. */ function drush_file_get_public() { drush_include_engine('drupal', 'environment'); return _drush_file_public_path(); } /** * Return the path to private files directory. */ function drush_file_get_private() { drush_include_engine('drupal', 'environment'); return _drush_file_private_path(); } /** * Returns the sitewide Drupal directory for extensions. */ function drush_drupal_sitewide_directory($major_version = NULL) { if (!$major_version) { $major_version = drush_drupal_major_version(); } return ($major_version < 8) ? 'sites/all' : ''; } /** * Helper function to get core compatibility constant. * * @return string * The Drupal core compatibility constant. */ function drush_get_drupal_core_compatibility() { if (defined('DRUPAL_CORE_COMPATIBILITY')) { return DRUPAL_CORE_COMPATIBILITY; } elseif (defined('\Drupal::CORE_COMPATIBILITY')) { return \Drupal::CORE_COMPATIBILITY; } } /** * Set Env. Variables for given site-alias. */ function drush_set_environment_vars(array $site_record) { if (!empty($site_record)) { $os = drush_os($site_record); if (isset($site_record['#env-vars'])) { foreach ($site_record['#env-vars'] as $var => $value) { $env_var = drush_escapeshellarg($var, $os, TRUE) . '=' . drush_escapeshellarg($value, $os, TRUE); putenv($env_var); } } } } &1', $output, $result); _drush_shell_exec_output_set($output); if (drush_get_context('DRUSH_DEBUG')) { foreach ($output as $line) { drush_print($line, 2); } } // Exit code 0 means success. return ($result == 0); } } else { return TRUE; } } /** * Determine whether 'which $command' can find * a command on this system. */ function drush_which($command) { exec("which $command 2>&1", $output, $result); return ($result == 0); } /** * Build an SSH string including an optional fragment of bash. Commands that use * this should also merge drush_shell_proc_build_options() into their * command options. @see ssh_drush_command(). * * @param array $site * A site alias record. * @param string $command * An optional bash fragment. * @param string $cd * An optional directory to change into before executing the $command. Set to * boolean TRUE to change into $site['root'] if available. * @param boolean $interactive * Force creation of a tty * @return string * A string suitable for execution with drush_shell_remote_exec(). */ function drush_shell_proc_build($site, $command = '', $cd = NULL, $interactive = FALSE) { // Build up the command. TODO: We maybe refactor this soon. $hostname = drush_remote_host($site); $ssh_options = drush_sitealias_get_option($site, 'ssh-options', "-o PasswordAuthentication=no"); $os = drush_os($site); if (drush_sitealias_get_option($site, 'tty') || $interactive) { $ssh_options .= ' -t'; } $cmd = "ssh " . $ssh_options . " " . $hostname; if ($cd === TRUE) { if (array_key_exists('root', $site)) { $cd = $site['root']; } else { $cd = FALSE; } } if ($cd) { $command = 'cd ' . drush_escapeshellarg($cd, $os) . ' && ' . $command; } if (!empty($command)) { if (!drush_get_option('escaped', FALSE)) { $cmd .= " " . drush_escapeshellarg($command, $os); } else { $cmd .= " $command"; } } return $cmd; } /** * Execute bash command using proc_open(). * * @returns * Exit code from launched application * 0 no error * 1 general error * 127 command not found */ function drush_shell_proc_open($cmd) { if (drush_get_context('DRUSH_VERBOSE') || drush_get_context('DRUSH_SIMULATE')) { drush_print("Calling proc_open($cmd);", 0, STDERR); } if (!drush_get_context('DRUSH_SIMULATE')) { $process = proc_open($cmd, array(0 => STDIN, 1 => STDOUT, 2 => STDERR), $pipes); $proc_status = proc_get_status($process); $exit_code = proc_close($process); return ($proc_status["running"] ? $exit_code : $proc_status["exitcode"] ); } return 0; } /** * Used by definition of ssh and other commands that call into drush_shell_proc_build() * to declare their options. */ function drush_shell_exec_proc_build_options() { return array( 'ssh-options' => 'A string of extra options that will be passed to the ssh command (e.g. "-p 100")', 'tty' => 'Create a tty (e.g. to run an interactive program).', 'escaped' => 'Command string already escaped; do not add additional quoting.', ); } /** * Determine the appropriate os value for the * specified site record * * @returns * NULL for 'same as local machine', 'Windows' or 'Linux'. */ function drush_os($site_record = NULL) { // Default to $os = NULL, meaning 'same as local machine' $os = NULL; // If the site record has an 'os' element, use it if (isset($site_record) && array_key_exists('os', $site_record)) { $os = $site_record['os']; } // Otherwise, we will assume that all remote machines are Linux // (or whatever value 'remote-os' is set to in drushrc.php). elseif (isset($site_record) && array_key_exists('remote-host', $site_record) && !empty($site_record['remote-host'])) { $os = drush_get_option('remote-os', 'Linux'); } return $os; } /** * Determine the remote host (username@hostname.tld) for * the specified site. */ function drush_remote_host($site, $prefix = '') { $hostname = drush_escapeshellarg(drush_sitealias_get_option($site, 'remote-host', '', $prefix), "LOCAL"); $username = drush_escapeshellarg(drush_sitealias_get_option($site, 'remote-user', '', $prefix), "LOCAL"); return $username . (empty($username) ? '' : '@') . $hostname; } /** * Make an attempt to simply wrap the arg with the * kind of quote characters it does not already contain. * If it contains both kinds, then this function reverts to drush_escapeshellarg. */ function drush_wrap_with_quotes($arg) { $has_double = strpos($arg, '"') !== FALSE; $has_single = strpos($arg, "'") !== FALSE; if ($has_double && $has_single) { return drush_escapeshellarg($arg); } elseif ($has_double) { return "'" . $arg . "'"; } else { return '"' . $arg . '"'; } } /** * Platform-dependent version of escapeshellarg(). * Given the target platform, return an appropriately-escaped * string. The target platform may be omitted for args that * are /known/ to be for the local machine. * Use raw to get an unquoted version of the escaped arg. * Notice that you can't add quotes later until you know the platform. */ /** * Stores output for the most recent shell command. * This should only be run from drush_shell_exec(). * * @param array|bool $output * The output of the most recent shell command. * If this is not set the stored value will be returned. */ function _drush_shell_exec_output_set($output = FALSE) { static $stored_output; if ($output === FALSE) return $stored_output; $stored_output = $output; } /** * Returns the output of the most recent shell command as an array of lines. */ function drush_shell_exec_output() { return _drush_shell_exec_output_set(); } /** * Starts a background browser/tab for the current site or a specified URL. * * Uses a non-blocking proc_open call, so Drush execution will continue. * * @param $uri * Optional URI or site path to open in browser. If omitted, or if a site path * is specified, the current site home page uri will be prepended if the sites * hostname resolves. * @return * TRUE if browser was opened, FALSE if browser was disabled by the user or a, * default browser could not be found. */ function drush_start_browser($uri = NULL, $sleep = FALSE, $port = FALSE) { if ($browser = drush_get_option('browser', TRUE)) { // We can only open a browser if we have a DISPLAY environment variable on // POSIX or are running Windows or OS X. if (!drush_get_context('DRUSH_SIMULATE') && !getenv('DISPLAY') && !drush_is_windows() && !drush_is_osx()) { drush_log(dt('No graphical display appears to be available, not starting browser.'), LogLevel::NOTICE); return FALSE; } $host = parse_url($uri, PHP_URL_HOST); if (!$host) { // Build a URI for the current site, if we were passed a path. $site = drush_get_context('DRUSH_URI'); $host = parse_url($site, PHP_URL_HOST); $uri = $site . '/' . ltrim($uri, '/'); } // Validate that the host part of the URL resolves, so we don't attempt to // open the browser for http://default or similar invalid hosts. $hosterror = (gethostbynamel($host) === FALSE); $iperror = (ip2long($host) && gethostbyaddr($host) == $host); if (!drush_get_context('DRUSH_SIMULATE') && ($hosterror || $iperror)) { drush_log(dt('!host does not appear to be a resolvable hostname or IP, not starting browser. You may need to use the --uri option in your command or site alias to indicate the correct URL of this site.', array('!host' => $host)), LogLevel::WARNING); return FALSE; } if ($port) { $uri = str_replace($host, "localhost:$port", $uri); } if ($browser === TRUE) { // See if we can find an OS helper to open URLs in default browser. if (drush_shell_exec('which xdg-open')) { $browser = 'xdg-open'; } else if (drush_shell_exec('which open')) { $browser = 'open'; } else if (!drush_has_bash()) { $browser = 'start'; } else { // Can't find a valid browser. $browser = FALSE; } } $prefix = ''; if ($sleep) { $prefix = 'sleep ' . $sleep . ' && '; } if ($browser) { drush_log(dt('Opening browser !browser at !uri', array('!browser' => $browser, '!uri' => $uri))); if (!drush_get_context('DRUSH_SIMULATE')) { $pipes = array(); proc_close(proc_open($prefix . $browser . ' ' . drush_escapeshellarg($uri) . ' 2> ' . drush_bit_bucket() . ' &', array(), $pipes)); } return TRUE; } } return FALSE; } /** * @} End of "defgroup commandwrappers". */ filename), $sum); $hashes[] = trim(str_replace(array($dir), array(''), $sum[0])); } sort($hashes); return md5(implode("\n", $hashes)); } /** * Deletes the specified file or directory and everything inside it. * * Usually respects read-only files and folders. To do a forced delete use * drush_delete_tmp_dir() or set the parameter $forced. * * @param string $dir * The file or directory to delete. * @param bool $force * Whether or not to try everything possible to delete the directory, even if * it's read-only. Defaults to FALSE. * @param bool $follow_symlinks * Whether or not to delete symlinked files. Defaults to FALSE--simply * unlinking symbolic links. * * @return bool * FALSE on failure, TRUE if everything was deleted. */ function drush_delete_dir($dir, $force = FALSE, $follow_symlinks = FALSE) { // Do not delete symlinked files, only unlink symbolic links if (is_link($dir) && !$follow_symlinks) { return unlink($dir); } // Allow to delete symlinks even if the target doesn't exist. if (!is_link($dir) && !file_exists($dir)) { return TRUE; } if (!is_dir($dir)) { if ($force) { // Force deletion of items with readonly flag. @chmod($dir, 0777); } return unlink($dir); } if (drush_delete_dir_contents($dir, $force) === FALSE) { return FALSE; } if ($force) { // Force deletion of items with readonly flag. @chmod($dir, 0777); } return rmdir($dir); } /** * Deletes the contents of a directory. * * @param string $dir * The directory to delete. * @param bool $force * Whether or not to try everything possible to delete the contents, even if * they're read-only. Defaults to FALSE. * * @return bool * FALSE on failure, TRUE if everything was deleted. */ function drush_delete_dir_contents($dir, $force = FALSE) { $scandir = @scandir($dir); if (!is_array($scandir)) { return FALSE; } foreach ($scandir as $item) { if ($item == '.' || $item == '..') { continue; } if ($force) { @chmod($dir, 0777); } if (!drush_delete_dir($dir . '/' . $item, $force)) { return FALSE; } } return TRUE; } /** * Deletes the provided file or folder and everything inside it. * This function explicitely tries to delete read-only files / folders. * * @param $dir * The directory to delete * @return * FALSE on failure, TRUE if everything was deleted */ function drush_delete_tmp_dir($dir) { return drush_delete_dir($dir, TRUE); } /** * Copy $src to $dest. * * @param $src * The directory to copy. * @param $dest * The destination to copy the source to, including the new name of * the directory. To copy directory "a" from "/b" to "/c", then * $src = "/b/a" and $dest = "/c/a". To copy "a" to "/c" and rename * it to "d", then $dest = "/c/d". * @param $overwrite * Action to take if destination already exists. * - FILE_EXISTS_OVERWRITE - completely removes existing directory. * - FILE_EXISTS_ABORT - aborts the operation. * - FILE_EXISTS_MERGE - Leaves existing files and directories in place. * @return * TRUE on success, FALSE on failure. */ function drush_copy_dir($src, $dest, $overwrite = FILE_EXISTS_ABORT) { // Preflight based on $overwrite if $dest exists. if (file_exists($dest)) { if ($overwrite === FILE_EXISTS_OVERWRITE) { drush_op('drush_delete_dir', $dest, TRUE); } elseif ($overwrite === FILE_EXISTS_ABORT) { return drush_set_error('DRUSH_DESTINATION_EXISTS', dt('Destination directory !dest already exists.', array('!dest' => $dest))); } elseif ($overwrite === FILE_EXISTS_MERGE) { // $overwrite flag may indicate we should merge instead. drush_log(dt('Merging existing !dest directory', array('!dest' => $dest))); } } // $src readable? if (!is_readable($src)) { return drush_set_error('DRUSH_SOURCE_NOT_EXISTS', dt('Source directory !src is not readable or does not exist.', array('!src' => $src))); } // $dest writable? if (!is_writable(dirname($dest))) { return drush_set_error('DRUSH_DESTINATION_NOT_WRITABLE', dt('Destination directory !dest is not writable.', array('!dest' => dirname($dest)))); } // Try to do a recursive copy. if (@drush_op('_drush_recursive_copy', $src, $dest)) { return TRUE; } return drush_set_error('DRUSH_COPY_DIR_FAILURE', dt('Unable to copy !src to !dest.', array('!src' => $src, '!dest' => $dest))); } /** * Internal function called by drush_copy_dir; do not use directly. */ function _drush_recursive_copy($src, $dest) { // all subdirectories and contents: if(is_dir($src)) { if (!drush_mkdir($dest, TRUE)) { return FALSE; } $dir_handle = opendir($src); while($file = readdir($dir_handle)) { if ($file != "." && $file != "..") { if (_drush_recursive_copy("$src/$file", "$dest/$file") !== TRUE) { return FALSE; } } } closedir($dir_handle); } elseif (is_link($src)) { symlink(readlink($src), $dest); } elseif (!copy($src, $dest)) { return FALSE; } // Preserve file modification time. // https://github.com/drush-ops/drush/pull/1146 touch($dest, filemtime($src)); // Preserve execute permission. if (!is_link($src) && !drush_is_windows()) { // Get execute bits of $src. $execperms = fileperms($src) & 0111; // Apply execute permissions if any. if ($execperms > 0) { $perms = fileperms($dest) | $execperms; chmod($dest, $perms); } } return TRUE; } /** * Move $src to $dest. * * If the php 'rename' function doesn't work, then we'll do copy & delete. * * @param $src * The directory to move. * @param $dest * The destination to move the source to, including the new name of * the directory. To move directory "a" from "/b" to "/c", then * $src = "/b/a" and $dest = "/c/a". To move "a" to "/c" and rename * it to "d", then $dest = "/c/d" (just like php rename function). * @param $overwrite * If TRUE, the destination will be deleted if it exists. * @return * TRUE on success, FALSE on failure. */ function drush_move_dir($src, $dest, $overwrite = FALSE) { // Preflight based on $overwrite if $dest exists. if (file_exists($dest)) { if ($overwrite) { drush_op('drush_delete_dir', $dest, TRUE); } else { return drush_set_error('DRUSH_DESTINATION_EXISTS', dt('Destination directory !dest already exists.', array('!dest' => $dest))); } } // $src readable? if (!drush_op('is_readable', $src)) { return drush_set_error('DRUSH_SOURCE_NOT_EXISTS', dt('Source directory !src is not readable or does not exist.', array('!src' => $src))); } // $dest writable? if (!drush_op('is_writable', dirname($dest))) { return drush_set_error('DRUSH_DESTINATION_NOT_WRITABLE', dt('Destination directory !dest is not writable.', array('!dest' => dirname($dest)))); } // Try rename. It will fail if $src and $dest are not in the same partition. if (@drush_op('rename', $src, $dest)) { return TRUE; } // Eventually it will create an empty file in $dest. See // http://www.php.net/manual/es/function.rename.php#90025 elseif (is_file($dest)) { drush_op('unlink', $dest); } // If 'rename' fails, then we will use copy followed // by a delete of the source. if (drush_copy_dir($src, $dest)) { drush_op('drush_delete_dir', $src, TRUE); return TRUE; } return drush_set_error('DRUSH_MOVE_DIR_FAILURE', dt('Unable to move !src to !dest.', array('!src' => $src, '!dest' => $dest))); } /** * Cross-platform compatible helper function to recursively create a directory tree. * * @param path * Path to directory to create. * @param required * If TRUE, then drush_mkdir will call drush_set_error on failure. * * Callers should *always* do their own error handling after calling drush_mkdir. * If $required is FALSE, then a different location should be selected, and a final * error message should be displayed if no usable locations can be found. * @see drush_directory_cache(). * If $required is TRUE, then the execution of the current command should be * halted if the required directory cannot be created. */ function drush_mkdir($path, $required = TRUE) { if (!is_dir($path)) { if (drush_mkdir(dirname($path))) { if (@mkdir($path)) { return TRUE; } elseif (is_dir($path) && is_writable($path)) { // The directory was created by a concurrent process. return TRUE; } else { if (!$required) { return FALSE; } if (is_writable(dirname($path))) { return drush_set_error('DRUSH_CREATE_DIR_FAILURE', dt('Unable to create !dir.', array('!dir' => preg_replace('/\w+\/\.\.\//', '', $path)))); } else { return drush_set_error('DRUSH_PARENT_NOT_WRITABLE', dt('Unable to create !newdir in !dir. Please check directory permissions.', array('!newdir' => basename($path), '!dir' => realpath(dirname($path))))); } } } return FALSE; } else { if (!is_writable($path)) { if (!$required) { return FALSE; } return drush_set_error('DRUSH_DESTINATION_NOT_WRITABLE', dt('Directory !dir exists, but is not writable. Please check directory permissions.', array('!dir' => realpath($path)))); } return TRUE; } } /* * Determine if program exists on user's PATH. * * @return bool|null */ function drush_program_exists($program) { if (drush_has_bash()) { $bucket = drush_bit_bucket(); return drush_op_system("command -v $program >$bucket 2>&1") === 0 ? TRUE : FALSE; } } /** * Save a string to a temporary file. Does not depend on Drupal's API. * The temporary file will be automatically deleted when drush exits. * * @param string $data * @param string $suffix * Append string to filename. use of this parameter if is discouraged. @see * drush_tempnam(). * @return string * A path to the file. */ function drush_save_data_to_temp_file($data, $suffix = NULL) { static $fp; $file = drush_tempnam('drush_', NULL, $suffix); $fp = fopen($file, "w"); fwrite($fp, $data); $meta_data = stream_get_meta_data($fp); $file = $meta_data['uri']; fclose($fp); return $file; } /** * Returns the path to a temporary directory. * * This is a custom version of Drupal's file_directory_path(). * We can't directly rely on sys_get_temp_dir() as this * path is not valid in some setups for Mac, and we want to honor * an environment variable (used by tests). */ function drush_find_tmp() { static $temporary_directory; if (!isset($temporary_directory)) { $directories = array(); // Get user specific and operating system temp folders from system environment variables. // See http://www.microsoft.com/resources/documentation/windows/xp/all/proddocs/en-us/ntcmds_shelloverview.mspx?mfr=true $tempdir = getenv('TEMP'); if (!empty($tempdir)) { $directories[] = $tempdir; } $tmpdir = getenv('TMP'); if (!empty($tmpdir)) { $directories[] = $tmpdir; } // Operating system specific dirs. if (drush_is_windows()) { $windir = getenv('WINDIR'); if (isset($windir)) { // WINDIR itself is not writable, but it always contains a /Temp dir, // which is the system-wide temporary directory on older versions. Newer // versions only allow system processes to use it. $directories[] = Path::join($windir, 'Temp'); } } else { $directories[] = Path::canonicalize('/tmp'); } $directories[] = Path::canonicalize(sys_get_temp_dir()); foreach ($directories as $directory) { if (is_dir($directory) && is_writable($directory)) { $temporary_directory = $directory; break; } } if (empty($temporary_directory)) { // If no directory has been found, create one in cwd. $temporary_directory = Path::join(drush_cwd(), 'tmp'); drush_mkdir($temporary_directory, TRUE); if (!is_dir($temporary_directory)) { return drush_set_error('DRUSH_UNABLE_TO_CREATE_TMP_DIR', dt("Unable to create a temporary directory.")); } drush_register_file_for_deletion($temporary_directory); } } return $temporary_directory; } /** * Creates a temporary file, and registers it so that * it will be deleted when drush exits. Whenever possible, * drush_save_data_to_temp_file() should be used instead * of this function. * * @param string $suffix * Append this suffix to the filename. Use of this parameter is discouraged as * it can break the guarantee of tempname(). See http://www.php.net/manual/en/function.tempnam.php#42052. * Originally added to support Oracle driver. */ function drush_tempnam($pattern, $tmp_dir = NULL, $suffix = '') { if ($tmp_dir == NULL) { $tmp_dir = drush_find_tmp(); } $tmp_file = tempnam($tmp_dir, $pattern); drush_register_file_for_deletion($tmp_file); $tmp_file_with_suffix = $tmp_file . $suffix; drush_register_file_for_deletion($tmp_file_with_suffix); return $tmp_file_with_suffix; } /** * Creates a temporary directory and return its path. */ function drush_tempdir() { $tmp_dir = drush_trim_path(drush_find_tmp()); $tmp_dir .= '/' . 'drush_tmp_' . uniqid(time() . '_'); drush_mkdir($tmp_dir); drush_register_file_for_deletion($tmp_dir); return $tmp_dir; } /** * Any file passed in to this function will be deleted * when drush exits. */ function drush_register_file_for_deletion($file = NULL) { static $registered_files = array(); if (isset($file)) { if (empty($registered_files)) { register_shutdown_function('_drush_delete_registered_files'); } $registered_files[] = $file; } return $registered_files; } /** * Delete all of the registered temporary files. */ function _drush_delete_registered_files() { $files_to_delete = drush_register_file_for_deletion(); foreach ($files_to_delete as $file) { // We'll make sure that the file still exists, just // in case someone came along and deleted it, even // though they did not need to. if (file_exists($file)) { if (is_dir($file)) { drush_delete_dir($file, TRUE); } else { @chmod($file, 0777); // Make file writeable unlink($file); } } } } /** * Decide where our backup directory should go * * @param string $subdir * The name of the desired subdirectory(s) under drush-backups. * Usually a database name. */ function drush_preflight_backup_dir($subdir = NULL) { $backup_dir = drush_get_context('DRUSH_BACKUP_DIR', drush_get_option('backup-location')); if (empty($backup_dir)) { // Try to use db name as subdir if none was provided. if (empty($subdir)) { $subdir = 'unknown'; if ($sql = drush_sql_get_class()) { $db_spec = $sql->db_spec(); $subdir = $db_spec['database']; } } // Save the date to be used in the backup directory's path name. $date = gmdate('YmdHis', $_SERVER['REQUEST_TIME']); $backup_dir = drush_get_option('backup-dir', Path::join(drush_server_home(), 'drush-backups')); $backup_dir = Path::join($backup_dir, $subdir, $date); drush_set_context('DRUSH_BACKUP_DIR', $backup_dir); } else { Path::canonicalize($backup_dir); } return $backup_dir; } /** * Prepare a backup directory */ function drush_prepare_backup_dir($subdir = NULL) { $backup_dir = drush_preflight_backup_dir($subdir); $backup_parent = Path::getDirectory($backup_dir); $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT'); if ((!empty($drupal_root)) && (strpos($backup_parent, $drupal_root) === 0)) { return drush_set_error('DRUSH_PM_BACKUP_FAILED', dt('It\'s not allowed to store backups inside the Drupal root directory.')); } if (!file_exists($backup_parent)) { if (!drush_mkdir($backup_parent, TRUE)) { return drush_set_error('DRUSH_PM_BACKUP_FAILED', dt('Unable to create backup directory !dir.', array('!dir' => $backup_parent))); } } if (!is_writable($backup_parent)) { return drush_set_error('DRUSH_PM_BACKUP_FAILED', dt('Backup directory !dir is not writable.', array('!dir' => $backup_parent))); } if (!drush_mkdir($backup_dir, TRUE)) { return FALSE; } return $backup_dir; } /** * Test to see if a file exists and is not empty */ function drush_file_not_empty($file_to_test) { if (file_exists($file_to_test)) { clearstatcache(); $stat = stat($file_to_test); if ($stat['size'] > 0) { return TRUE; } } return FALSE; } /** * Finds all files that match a given mask in a given directory. * Directories and files beginning with a period are excluded; this * prevents hidden files and directories (such as SVN working directories * and GIT repositories) from being scanned. * * @param $dir * The base directory for the scan, without trailing slash. * @param $mask * The regular expression of the files to find. * @param $nomask * An array of files/directories to ignore. * @param $callback * The callback function to call for each match. * @param $recurse_max_depth * When TRUE, the directory scan will recurse the entire tree * starting at the provided directory. When FALSE, only files * in the provided directory are returned. Integer values * limit the depth of the traversal, with zero being treated * identically to FALSE, and 1 limiting the traversal to the * provided directory and its immediate children only, and so on. * @param $key * The key to be used for the returned array of files. Possible * values are "filename", for the path starting with $dir, * "basename", for the basename of the file, and "name" for the name * of the file without an extension. * @param $min_depth * Minimum depth of directories to return files from. * @param $include_dot_files * If TRUE, files that begin with a '.' will be returned if they * match the provided mask. If FALSE, files that begin with a '.' * will not be returned, even if they match the provided mask. * @param $depth * Current depth of recursion. This parameter is only used internally and should not be passed. * * @return * An associative array (keyed on the provided key) of objects with * "path", "basename", and "name" members corresponding to the * matching files. */ function drush_scan_directory($dir, $mask, $nomask = array('.', '..', 'CVS'), $callback = 0, $recurse_max_depth = TRUE, $key = 'filename', $min_depth = 0, $include_dot_files = FALSE, $depth = 0) { $key = (in_array($key, array('filename', 'basename', 'name')) ? $key : 'filename'); $files = array(); // Exclude Bower and Node directories. $nomask = array_merge($nomask, array('node_modules', 'bower_components')); if (is_string($dir) && is_dir($dir) && $handle = opendir($dir)) { while (FALSE !== ($file = readdir($handle))) { if (!in_array($file, $nomask) && (($include_dot_files && (!preg_match("/\.\+/",$file))) || ($file[0] != '.'))) { if (is_dir("$dir/$file") && (($recurse_max_depth === TRUE) || ($depth < $recurse_max_depth))) { // Give priority to files in this folder by merging them in after any subdirectory files. $files = array_merge(drush_scan_directory("$dir/$file", $mask, $nomask, $callback, $recurse_max_depth, $key, $min_depth, $include_dot_files, $depth + 1), $files); } elseif ($depth >= $min_depth && preg_match($mask, $file)) { // Always use this match over anything already set in $files with the same $$key. $filename = "$dir/$file"; $basename = basename($file); $name = substr($basename, 0, strrpos($basename, '.')); $files[$$key] = new stdClass(); $files[$$key]->filename = $filename; $files[$$key]->basename = $basename; $files[$$key]->name = $name; if ($callback) { drush_op($callback, $filename); } } } } closedir($handle); } return $files; } /** * Simple helper function to append data to a given file. * * @param string $file * The full path to the file to append the data to. * @param string $data * The data to append. * * @return boolean * TRUE on success, FALSE in case of failure to open or write to the file. */ function drush_file_append_data($file, $data) { if (!$fd = fopen($file, 'a+')) { drush_set_error(dt("ERROR: fopen(@file, 'ab') failed", array('@file' => $file))); return FALSE; } if (!fwrite($fd, $data)) { drush_set_error(dt("ERROR: fwrite(@file) failed", array('@file' => $file)) . '
    ' . $data);
        return FALSE;
      }
      return TRUE;
    }
    
    /**
     * Return 'TRUE' if one directory is located anywhere inside
     * the other.
     */
    function drush_is_nested_directory($base_dir, $test_is_nested) {
      $common = Path::getLongestCommonBasePath([$test_is_nested, $base_dir]);
      return $common == Path::canonicalize($base_dir);
    }
    
    /**
     * @} End of "defgroup filesystemfunctions".
     */
     $metadata);
      }
      if (!is_array($metadata)) {
        $metadata = array();
      }
      $metadata += array(
        'metameta' => array(),
      );
      if (isset($metadata['format'])) {
        // If the format is set in metadata, then it will
        // override whatever is passed in via the $format parameter.
        $format = $metadata['format'];
      }
      if (!isset($format)) {
        // TODO: we shouldn't ever call drush_get_option here.
        // Get rid of this once we confirm that there are no
        // callers that still need it.
        $format = drush_get_option('format', 'print-r');
      }
    
      $formatter = drush_load_engine('outputformat', $format);
      if ($formatter) {
        if ($formatter === TRUE) {
          return drush_set_error(dt('No outputformat class defined for !format', array('!format' => $format)));
        }
        $output = $formatter->process($input, $metadata);
      }
    
      return $output;
    }
    
    /**
     * Rudimentary replacement for Drupal API t() function.
     *
     * @param string
     *   String to process, possibly with replacement item.
     * @param array
     *  An associative array of replacement items.
     *
     * @return
     *   The processed string.
     *
     * @see t()
     */
    function dt($string, $args = array()) {
      $output = NULL;
      if (function_exists('t') && drush_drupal_major_version() == 7) {
        $output = t($string, $args);
      }
      // The language system requires a working container which has the string
      // translation service.
      else if (drush_drupal_major_version() >= 8 && \Drupal::hasService('string_translation')) {
        // Drupal 8 removes !var replacements, creating a user-level error when
        // these are used, so we'll pre-replace these before calling translate().
        list($string, $args) = replace_legacy_dt_args($string, $args);
        $output = (string) \Drupal::translation()->translate($string, $args);
      }
      else if (function_exists('t') && drush_drupal_major_version() <= 7 && function_exists('theme')) {
        $output = t($string, $args);
      }
    
      // If Drupal's t() function unavailable.
      if (!isset($output)) {
        if (!empty($args)) {
          $output = strtr($string, $args);
        }
        else {
          $output = $string;
        }
      }
      return $output;
    }
    
    /**
     * Replace placeholders that begin with a '!' with '@'.
     */
    function replace_legacy_dt_args(&$string, &$legacy_args) {
      $args = array();
      $replace = array();
      foreach ($legacy_args as $name => $argument) {
        if ($name[0] == '!') {
          $new_arg = '@' . substr($name, 1);
          $replace[$name] = $new_arg;
          $args[$new_arg] = Markup::create($argument);
        }
        else {
          $args[$name] = $argument;
        }
      }
      return [
        strtr($string, $replace),
        $args
      ];
    }
    
    /**
     * Convert html to readable text.  Compatible API to
     * drupal_html_to_text, but less functional.  Caller
     * might prefer to call drupal_html_to_text if there
     * is a bootstrapped Drupal site available.
     *
     * @param string $html
     *   The html text to convert.
     *
     * @return string
     *   The plain-text representation of the input.
     */
    function drush_html_to_text($html, $allowed_tags = NULL) {
      $replacements = array(
        '
    ' => '------------------------------------------------------------------------------', '
  • ' => ' * ', '

    ' => '===== ', '

    ' => ' =====', '

    ' => '---- ', '

    ' => ' ----', '

    ' => '::: ', '

    ' => ' :::', '
    ' => "\n", ); $text = str_replace(array_keys($replacements), array_values($replacements), $html); return html_entity_decode(preg_replace('/ *<[^>]*> */', ' ', $text)); } /** * Print a formatted table. * * @param $rows * The rows to print. * @param $header * If TRUE, the first line will be treated as table header. * @param $widths * An associative array whose keys are column IDs and values are widths of each column (in characters). * If not specified this will be determined automatically, based on a "best fit" algorithm. * @param $handle * File handle to write to. NULL will write * to standard output, STDERR will write to the standard * error. See http://php.net/manual/en/features.commandline.io-streams.php * @return $tbl * Use $tbl->getTable() to get the output from the return value. */ function drush_print_table($rows, $header = FALSE, $widths = array(), $handle = NULL) { $tbl = _drush_format_table($rows, $header, $widths); $output = $tbl->getTable(); if (!stristr(PHP_OS, 'WIN')) { $output = str_replace("\r\n", PHP_EOL, $output); } drush_print(rtrim($output), 0, $handle); return $tbl; } /** * Format a table of data. * * @param $rows * The rows to print. * @param $header * If TRUE, the first line will be treated as table header. * @param $widths * An associative array whose keys are column IDs and values are widths of each column (in characters). * If not specified this will be determined automatically, based on a "best fit" algorithm. * @param array $console_table_options * An array that is passed along when constructing a Console_Table instance. * @return $output * The formatted output. */ function drush_format_table($rows, $header = FALSE, $widths = array(), $console_table_options = array()) { $tbl = _drush_format_table($rows, $header, $widths, $console_table_options); $output = $tbl->getTable(); if (!drush_is_windows()) { $output = str_replace("\r\n", PHP_EOL, $output); } return $output; } function _drush_format_table($rows, $header = FALSE, $widths = array(), $console_table_options = array()) { // Add defaults. $tbl = new ReflectionClass('Console_Table'); $console_table_options += array(CONSOLE_TABLE_ALIGN_LEFT , ''); $tbl = $tbl->newInstanceArgs($console_table_options); $auto_widths = drush_table_column_autowidth($rows, $widths); // Do wordwrap on all cells. $newrows = array(); foreach ($rows as $rowkey => $row) { foreach ($row as $col_num => $cell) { $newrows[$rowkey][$col_num] = wordwrap($cell, $auto_widths[$col_num], "\n", TRUE); if (isset($widths[$col_num])) { $newrows[$rowkey][$col_num] = str_pad($newrows[$rowkey][$col_num], $widths[$col_num]); } } } if ($header) { $headers = array_shift($newrows); $tbl->setHeaders($headers); } $tbl->addData($newrows); return $tbl; } /** * Convert an associative array of key : value pairs into * a table suitable for processing by drush_print_table. * * @param $keyvalue_table * An associative array of key : value pairs. * @param $metadata * 'key-value-item': If the value is an array, then * the item key determines which item from the value * will appear in the output. * @return * An array of arrays, where the keys from the input * array are stored in the first column, and the values * are stored in the third. A second colum is created * specifically to hold the ':' separator. */ function drush_key_value_to_array_table($keyvalue_table, $metadata = array()) { if (!is_array($keyvalue_table)) { return drush_set_error('DRUSH_INVALID_FORMAT', dt("Data not compatible with selected key-value output format.")); } if (!is_array($metadata)) { $metadata = array('key-value-item' => $metadata); } $item_key = array_key_exists('key-value-item', $metadata) ? $metadata['key-value-item'] : NULL; $metadata += array( 'format' => 'list', 'separator' => ' ', ); $table = array(); foreach ($keyvalue_table as $key => $value) { if (isset($value)) { if (is_array($value) && isset($item_key)) { $value = $value[$item_key]; } // We should only have simple values here, but if // we don't, use drush_format() to flatten as a fallback. if (is_array($value)) { $value = drush_format($value, $metadata, 'list'); } } if (isset($metadata['include-field-labels']) && !$metadata['include-field-labels']) { $table[] = array(isset($value) ? $value : ''); } elseif (isset($value)) { $table[] = array($key, ' :', $value); } else { $table[] = array($key . ':', '', ''); } } return $table; } /** * Select the fields that should be used. */ function drush_select_fields($all_field_labels, $fields, $strict = TRUE) { $field_labels = array(); foreach ($fields as $field) { if (array_key_exists($field, $all_field_labels)) { $field_labels[$field] = $all_field_labels[$field]; } else { // Allow the user to select fields via their human-readable names. // This is less convenient than the field name (since the human-readable // names may contain spaces, and must therefore be quoted), but these are // the values that the user sees in the command output. n.b. the help // text lists fields by their more convenient machine names. $key = array_search(strtolower($field), array_map('strtolower', $all_field_labels)); if ($key !== FALSE) { $field_labels[$key] = $all_field_labels[$key]; } elseif (!$strict) { $field_labels[$field] = $field; } } } return $field_labels; } /** * Select the fields from the input array that should be output. * * @param $input * An associative array of key:value pairs to be output * @param $fields * An associative array that maps FROM a field in $input * TO the corresponding field name in $output. * @param $mapping * An associative array that maps FROM a field in $fields * TO the actual field in $input to use in the preceeding * translation described above. * @return * The input array, re-ordered and re-keyed per $fields */ function drush_select_output_fields($input, $fields, $mapping = array(), $default_value = NULL) { $result = array(); if (empty($fields)) { $result = $input; } else { foreach ($fields as $key => $label) { $value = drush_lookup_field_by_path($input, $key, $mapping, $default_value); if (isset($value)) { $result[$label] = $value; } } } return $result; } /** * Return a specific item inside an array, as identified * by the provided path. * * @param $input: * An array of items, potentially multiple layers deep. * @param $path: * A specifier of array keys, either in an array or separated by * a '/', that list the elements of the array to access. This * works much like a very simple version of xpath for arrays, with * all items being treated identically (like elements). * @param $mapping: * (optional) An array whose keys may correspond to the $path parameter and * whose values are the corresponding paths to be used in $input. * * Example 1: * * $input = array('#name' => 'site.dev', '#id' => '222'); * $path = '#name'; * result: 'site.dev'; * * Example 2: * * $input = array('ca' => array('sf' => array('mission'=>array('1700'=>'woodward')))); * $path = 'ca/sf/mission/1701'; * result: 'woodward' * * Example 3: * * $input = array('#name' => 'site.dev', '#id' => '222'); * $path = 'name'; * $mapping = array('name' => '#name'); * result: 'site.dev'; */ function drush_lookup_field_by_path($input, $path, $mapping = array(), $default_value = NULL) { $result = ''; if (isset($mapping[$path])) { $path = $mapping[$path]; } if (!is_array($path)) { $parts = explode('/', $path); } if (!empty($parts)) { $result = $input; foreach ($parts as $key) { if ((is_array($result)) && (isset($result[$key]))) { $result = $result[$key]; } else { return $default_value; } } } return $result; } /** * Given a table array (an associative array of associative arrays), * return an array of all of the values with the specified field name. */ function drush_output_get_selected_field($input, $field_name, $default_value = '') { $result = array(); foreach ($input as $key => $data) { if (is_array($data) && isset($data[$field_name])) { $result[] = $data[$field_name]; } else { $result[] = $default_value; } } return $result; } /** * Hide any fields that are empty */ function drush_hide_empty_fields($input, $fields) { $has_data = array(); foreach ($input as $key => $data) { foreach ($fields as $field => $label) { if (isset($data[$field]) && !empty($data[$field])) { $has_data[$field] = TRUE; } } } foreach ($fields as $field => $label) { if (!isset($has_data[$field])) { unset($fields[$field]); } } return $fields; } /** * Convert an array of data rows, where each row contains an * associative array of key : value pairs, into * a table suitable for processing by drush_print_table. * The provided $header determines the order that the items * will appear in the output. Only data items listed in the * header will be placed in the output. * * @param $rows_of_keyvalue_table * array( * 'row1' => array('col1' => 'data', 'col2' => 'data'), * 'row2' => array('col1' => 'data', 'col2' => 'data'), * ) * @param $header * array('col1' => 'Column 1 Label', 'col2' => 'Column 2 Label') * @param $metadata * (optional) An array of special options, all optional: * - strip-tags: call the strip_tags function on the data * before placing it in the table * - concatenate-columns: an array of: * - dest-col: array('src-col1', 'src-col2') * Appends all of the src columns with whatever is * in the destination column. Appended columns are * separated by newlines. * - transform-columns: an array of: * - dest-col: array('from' => 'to'), * Transforms any occurance of 'from' in 'dest-col' to 'to'. * - format-cell: Drush output format name to use to format * any cell that is an array. * - process-cell: php function name to use to process * any cell that is an array. * - field-mappings: an array whose keys are some or all of the keys in * $header and whose values are the corresponding keys to use when * indexing the values of $rows_of_keyvalue_table. * @return * An array of arrays */ function drush_rows_of_key_value_to_array_table($rows_of_keyvalue_table, $header, $metadata) { if (isset($metadata['hide-empty-fields'])) { $header = drush_hide_empty_fields($rows_of_keyvalue_table, $header); } if (empty($header)) { $first = (array)reset($rows_of_keyvalue_table); $keys = array_keys($first); $header = array_combine($keys, $keys); } $table = array(array_values($header)); if (isset($rows_of_keyvalue_table) && is_array($rows_of_keyvalue_table) && !empty($rows_of_keyvalue_table)) { foreach ($rows_of_keyvalue_table as $row_index => $row_data) { $row_data = (array)$row_data; $row = array(); foreach ($header as $column_key => $column_label) { $data = drush_lookup_field_by_path($row_data, $column_key, $metadata['field-mappings']); if (array_key_exists('transform-columns', $metadata)) { foreach ($metadata['transform-columns'] as $dest_col => $transformations) { if ($dest_col == $column_key) { $data = str_replace(array_keys($transformations), array_values($transformations), $data); } } } if (array_key_exists('concatenate-columns', $metadata)) { foreach ($metadata['concatenate-columns'] as $dest_col => $src_cols) { if ($dest_col == $column_key) { $data = ''; if (!is_array($src_cols)) { $src_cols = array($src_cols); } foreach($src_cols as $src) { if (array_key_exists($src, $row_data) && !empty($row_data[$src])) { if (!empty($data)) { $data .= "\n"; } $data .= $row_data[$src]; } } } } } if (array_key_exists('format-cell', $metadata) && is_array($data)) { $data = drush_format($data, array(), $metadata['format-cell']); } if (array_key_exists('process-cell', $metadata) && is_array($data)) { $data = $metadata['process-cell']($data, $metadata); } if (array_key_exists('strip-tags', $metadata)) { $data = strip_tags($data); } $row[] = $data; } $table[] = $row; } } return $table; } /** * Determine the best fit for column widths. * * @param $rows * The rows to use for calculations. * @param $widths * Manually specified widths of each column (in characters) - these will be * left as is. */ function drush_table_column_autowidth($rows, $widths) { $auto_widths = $widths; // First we determine the distribution of row lengths in each column. // This is an array of descending character length keys (i.e. starting at // the rightmost character column), with the value indicating the number // of rows where that character column is present. $col_dist = array(); foreach ($rows as $rowkey => $row) { foreach ($row as $col_id => $cell) { if (empty($widths[$col_id])) { $length = strlen($cell); if ($length == 0) { $col_dist[$col_id][0] = 0; } while ($length > 0) { if (!isset($col_dist[$col_id][$length])) { $col_dist[$col_id][$length] = 0; } $col_dist[$col_id][$length]++; $length--; } } } } foreach ($col_dist as $col_id => $count) { // Sort the distribution in decending key order. krsort($col_dist[$col_id]); // Initially we set all columns to their "ideal" longest width // - i.e. the width of their longest column. $auto_widths[$col_id] = max(array_keys($col_dist[$col_id])); } // We determine what width we have available to use, and what width the // above "ideal" columns take up. $available_width = drush_get_context('DRUSH_COLUMNS', 80) - (count($auto_widths) * 2); $auto_width_current = array_sum($auto_widths); // If we need to reduce a column so that we can fit the space we use this // loop to figure out which column will cause the "least wrapping", // (relative to the other columns) and reduce the width of that column. while ($auto_width_current > $available_width) { $count = 0; $width = 0; foreach ($col_dist as $col_id => $counts) { // If we are just starting out, select the first column. if ($count == 0 || // OR: if this column would cause less wrapping than the currently // selected column, then select it. (current($counts) < $count) || // OR: if this column would cause the same amount of wrapping, but is // longer, then we choose to wrap the longer column (proportionally // less wrapping, and helps avoid triple line wraps). (current($counts) == $count && key($counts) > $width)) { // Select the column number, and record the count and current width // for later comparisons. $column = $col_id; $count = current($counts); $width = key($counts); } } if ($width <= 1) { // If we have reached a width of 1 then give up, so wordwrap can still progress. break; } // Reduce the width of the selected column. $auto_widths[$column]--; // Reduce our overall table width counter. $auto_width_current--; // Remove the corresponding data from the disctribution, so next time // around we use the data for the row to the left. unset($col_dist[$column][$width]); } return $auto_widths; } /** * Print the contents of a file. * * @param string $file * Full path to a file. */ function drush_print_file($file) { // Don't even bother to print the file in --no mode if (drush_get_context('DRUSH_NEGATIVE')) { return; } if ((substr($file,-4) == ".htm") || (substr($file,-5) == ".html")) { $tmp_file = drush_tempnam(basename($file)); file_put_contents($tmp_file, drush_html_to_text(file_get_contents($file))); $file = $tmp_file; } // Do not wait for user input in --yes or --pipe modes if (drush_get_context('DRUSH_PIPE')) { drush_print_pipe(file_get_contents($file)); } elseif (drush_get_context('DRUSH_AFFIRMATIVE')) { drush_print(file_get_contents($file)); } elseif (drush_shell_exec_interactive("less %s", $file)) { return; } elseif (drush_shell_exec_interactive("more %s", $file)) { return; } else { drush_print(file_get_contents($file)); } } /** * Converts a PHP variable into its Javascript equivalent. * * We provide a copy of D7's drupal_json_encode since this function is * unavailable on earlier versions of Drupal. * * @see drupal_json_decode() * @ingroup php_wrappers */ function drush_json_encode($var) { if (version_compare(phpversion(), '5.4.0', '>=')) { $json = json_encode($var, JSON_PRETTY_PRINT); } else { $json = json_encode($var); } // json_encode() does not escape <, > and &, so we do it with str_replace(). return str_replace(array('<', '>', '&'), array('\u003c', '\u003e', '\u0026'), $json); } /** * Converts an HTML-safe JSON string into its PHP equivalent. * * We provide a copy of D7's drupal_json_decode since this function is * unavailable on earlier versions of Drupal. * * @see drupal_json_encode() * @ingroup php_wrappers */ function drush_json_decode($var) { return json_decode($var, TRUE); } /** * Drupal-friendly var_export(). Taken from utility.inc in Drupal 8. * * @param $var * The variable to export. * @param $prefix * A prefix that will be added at the beginning of every lines of the output. * * @return * The variable exported in a way compatible to Drupal's coding standards. */ function drush_var_export($var, $prefix = '') { if (is_array($var)) { if (empty($var)) { $output = 'array()'; } else { $output = "array(\n"; // Don't export keys if the array is non associative. $export_keys = array_values($var) != $var; foreach ($var as $key => $value) { $output .= ' ' . ($export_keys ? drush_var_export($key) . ' => ' : '') . drush_var_export($value, ' ', FALSE) . ",\n"; } $output .= ')'; } } elseif (is_bool($var)) { $output = $var ? 'TRUE' : 'FALSE'; } elseif (is_string($var)) { $line_safe_var = str_replace("\n", '\n', $var); if (strpos($var, "\n") !== FALSE || strpos($var, "'") !== FALSE) { // If the string contains a line break or a single quote, use the // double quote export mode. Encode backslash and double quotes and // transform some common control characters. $var = str_replace(array('\\', '"', "\n", "\r", "\t"), array('\\\\', '\"', '\n', '\r', '\t'), $var); $output = '"' . $var . '"'; } else { $output = "'" . $var . "'"; } } elseif (is_object($var) && get_class($var) === 'stdClass') { // var_export() will export stdClass objects using an undefined // magic method __set_state() leaving the export broken. This // workaround avoids this by casting the object as an array for // export and casting it back to an object when evaluated. $output = '(object) ' . drush_var_export((array) $var, $prefix); } else { $output = var_export($var, TRUE); } if ($prefix) { $output = str_replace("\n", "\n$prefix", $output); } return $output; } /** * @} End of "defgroup outputfunctions". */ bootstrap_and_dispatch(); } } } // TODO: Get rid of global variable access here, and just trust // the bootstrap object returned from drush_preflight(). This will // require some adjustments to Drush bootstrapping. // See: https://github.com/drush-ops/drush/pull/1303 if ($bootstrap = drush_get_bootstrap_object()) { $bootstrap->terminate(); } drush_postflight(); if (is_object($return)) { $return = 0; } // How strict are we? If we are very strict, turn 'ok' into 'error' // if there are any warnings in the log. if (($return == 0) && (drush_get_option('strict') > 1) && drush_log_has_errors()) { $return = 1; } // After this point the drush_shutdown function will run, // exiting with the correct exit code. return $return; } /** * Prepare Drush for preflight. * * Runs before drush_main(). * * @see drush_main() * @see drush.php */ function drush_preflight_prepare() { define('DRUSH_BASE_PATH', dirname(dirname(__FILE__))); // Local means that autoload.php is inside of Drush. That is, Drush is its own Composer project. // Global means autoload.php is outside of Drush. That is, Drush is a dependency of a bigger project. $local_vendor_path = DRUSH_BASE_PATH . '/vendor/autoload.php'; $global_vendor_path = DRUSH_BASE_PATH . '/../../../vendor/autoload.php'; // Check for a local composer install or a global composer install. Vendor dirs are in different spots). if (file_exists($local_vendor_path)) { $vendor_path = $local_vendor_path; } elseif (file_exists($global_vendor_path)) { $vendor_path = $global_vendor_path; } else { $msg = "Unable to load autoload.php. Run composer install to fetch dependencies and write this file (http://docs.drush.org/en/master/install-alternative/). Or if you prefer, use the drush.phar which already has dependencies included (http://docs.drush.org/en/master/install).\n"; fwrite(STDERR, $msg); return FALSE; } $classloader = require $vendor_path; require_once DRUSH_BASE_PATH . '/includes/startup.inc'; require_once DRUSH_BASE_PATH . '/includes/bootstrap.inc'; require_once DRUSH_BASE_PATH . '/includes/environment.inc'; require_once DRUSH_BASE_PATH . '/includes/annotationcommand_adapter.inc'; require_once DRUSH_BASE_PATH . '/includes/command.inc'; require_once DRUSH_BASE_PATH . '/includes/drush.inc'; require_once DRUSH_BASE_PATH . '/includes/engines.inc'; require_once DRUSH_BASE_PATH . '/includes/backend.inc'; require_once DRUSH_BASE_PATH . '/includes/batch.inc'; require_once DRUSH_BASE_PATH . '/includes/context.inc'; require_once DRUSH_BASE_PATH . '/includes/sitealias.inc'; require_once DRUSH_BASE_PATH . '/includes/exec.inc'; require_once DRUSH_BASE_PATH . '/includes/drupal.inc'; require_once DRUSH_BASE_PATH . '/includes/output.inc'; require_once DRUSH_BASE_PATH . '/includes/cache.inc'; require_once DRUSH_BASE_PATH . '/includes/filesystem.inc'; require_once DRUSH_BASE_PATH . '/includes/dbtng.inc'; require_once DRUSH_BASE_PATH . '/includes/array_column.inc'; // Stash our vendor path and classloader. drush_set_context('DRUSH_VENDOR_PATH', dirname($vendor_path)); drush_set_context('DRUSH_CLASSLOADER', $classloader); // Can't log until we have a logger, so we'll create this ASAP. _drush_create_default_logger(); // Terminate immediately unless invoked as a command line script if (!drush_verify_cli()) { return drush_set_error('DRUSH_REQUIREMENTS_ERROR', dt('Drush is designed to run via the command line.')); } // Check supported version of PHP. // Note: If this is adjusted, check other code that compares // PHP_VERSION, such as drush_json_encode(), runserver/runserver.drush.inc, and also // adjust _drush_environment_check_php_ini() and the php_prohibited_options // list in the drush script. See http://drupal.org/node/1748228 define('DRUSH_MINIMUM_PHP', '5.4.5'); if (version_compare(phpversion(), DRUSH_MINIMUM_PHP) < 0 && !getenv('DRUSH_NO_MIN_PHP')) { return drush_set_error('DRUSH_REQUIREMENTS_ERROR', dt('Your command line PHP installation is too old. Drush requires at least PHP !version. To suppress this check, set the environment variable DRUSH_NO_MIN_PHP=1', array('!version' => DRUSH_MINIMUM_PHP))); } if (!$return = _drush_environment_check_php_ini()) { return; // An error was logged. } $drush_info = drush_read_drush_info(); define('DRUSH_VERSION', $drush_info['drush_version']); $version_parts = explode('.', DRUSH_VERSION); define('DRUSH_MAJOR_VERSION', $version_parts[0]); define('DRUSH_MINOR_VERSION', $version_parts[1]); define('DRUSH_REQUEST_TIME', microtime(TRUE)); drush_set_context('argc', $GLOBALS['argc']); drush_set_context('argv', $GLOBALS['argv']); // Set an error handler and a shutdown function set_error_handler('drush_error_handler'); register_shutdown_function('drush_shutdown'); // We need some global options/arguments processed at this early stage. drush_parse_args(); // Process initial global options such as --debug. _drush_preflight_global_options(); drush_log(dt("Drush preflight prepare loaded autoloader at !autoloader", array('!autoloader' => realpath($vendor_path))), LogLevel::PREFLIGHT); } /** * During the initialization of Drush, this is the first * step where we load our configuration and commandfiles, * and select the site we are going to operate on; however, * we take no irreversible actions (e.g. site bootstrapping). * This allows commands that are declared with no bootstrap * to select a new site root and bootstrap it. * * In this step we will register the shutdown function, * parse the command line arguments and store them in their * related contexts. * * Configuration files (drushrc.php) that are * a) Specified on the command line * b) Stored in the root directory of drush.php * c) Stored in the home directory of the system user. * * Additionally the DRUSH_QUIET and DRUSH_BACKEND contexts, * will be evaluated now, as they need to be set very early in * the execution flow to be able to take affect. * * @return \Drush\Boot\Boot; */ function drush_preflight() { // Create an alias '@none' to represent no Drupal site _drush_sitealias_cache_alias('@none', array('root' => '', 'uri' => '')); // Discover terminal width for pretty output. _drush_preflight_columns(); // Display is tidy now that column width has been handled. drush_log(dt('Starting Drush preflight.'), LogLevel::PREFLIGHT); // Statically define a way to call drush again. define('DRUSH_COMMAND', drush_find_drush()); // prime the CWD cache drush_cwd(); // Set up base environment for system-wide file locations. _drush_preflight_base_environment(); // Setup global alias_paths[] in context system. if (!drush_get_option('local')) { _drush_preflight_alias_path(); } if (!drush_get_option('local')) { // Load a drushrc.php file in the drush.php's directory. drush_load_config('drush'); // Load a drushrc.php file in the $ETC_PREFIX/etc/drush directory. drush_load_config('system'); // Load a drushrc.php file at ~/.drushrc.php. drush_load_config('user'); // Load a drushrc.php file in the ~/.drush directory. drush_load_config('home.drush'); } // Load a custom config specified with the --config option. drush_load_config('custom'); _drush_preflight_global_options(); // Load all the commandfiles findable from any of the // scopes listed above. _drush_find_commandfiles_drush(); // Look up the alias identifier that the user wants to use, // either via an argument or via 'site-set'. $target_alias = drush_sitealias_check_arg_and_site_set(); // Process the site alias that specifies which instance // of Drush (local or remote) this command will operate on. // We must do this after we load our config files (so that // site aliases are available), but before the rest of // Drush preflight and Drupal root bootstrap phase are // done, since site aliases may set option values that // affect these phases. $alias_record = _drush_sitealias_set_context_by_name($target_alias); // Find the selected site based on --root, --uri or cwd drush_preflight_root(); // Preflight the selected site, and load any configuration and commandfiles associated with it. drush_preflight_site(); // Check to see if anything changed during the 'site' preflight // that might allow us to find our alias record now if (empty($alias_record)) { $alias_record = _drush_sitealias_set_context_by_name($target_alias); // If the site alias settings changed late in the preflight, // then run the preflight for the root and site contexts again. if (!empty($alias_record)) { $remote_host = drush_get_option('remote-host'); if (!isset($remote_host)) { drush_preflight_root(); drush_preflight_site(); } } } // Fail if we could not find the selected site alias. if ($target_alias && empty($alias_record)) { // We will automatically un-set the site-set alias if it could not be found. // Otherwise, we'd be stuck -- the user would only be able to execute Drush // commands again after `drush @none site-set @none`, and most folks would // have a hard time figuring that out. $site_env = drush_sitealias_site_get(); if ($site_env == $target_alias) { drush_sitealias_site_clear(); } return drush_set_error('DRUSH_BOOTSTRAP_NO_ALIAS', dt("Could not find the alias !alias", array('!alias' => $target_alias))); } // If applicable swaps in shell alias values. drush_shell_alias_replace($target_alias); // Copy global options to their respective contexts _drush_preflight_global_options(); // Set environment variables based on #env-vars. drush_set_environment_vars($alias_record); // Select the bootstrap object and return it. return drush_select_bootstrap_class(); } /** * If --root is provided, set context. */ function drush_preflight_root() { $root = drush_get_option('root'); if (!isset($root)) { $root = drush_locate_root(); } if ($root) { $root = realpath($root); } // @todo This context name should not mention Drupal. // @todo Drupal code should use DRUSH_DRUPAL_ROOT instead of this constant. drush_set_context('DRUSH_SELECTED_DRUPAL_ROOT', $root); // Load the config options from Drupal's /drush, ../drush, and sites/all/drush directories, // even prior to bootstrapping the root. drush_load_config('drupal'); // Search for commandfiles in the root locations $discovery = annotationcommand_adapter_get_discovery(); $searchpath = [dirname($root) . '/drush', "$root/drush", "$root/sites/all/drush"]; $drush_root_extensions = $discovery->discover($searchpath, '\Drush'); drush_set_context( 'DRUSH_ANNOTATED_COMMANDFILES', array_merge( drush_get_context('DRUSH_ANNOTATED_COMMANDFILES'), $drush_root_extensions ) ); } function drush_preflight_site() { // Load the Drupal site configuration options upfront. drush_load_config('site'); // Determine URI and set constants/contexts accordingly. Keep this after loading of drupal,site configs. _drush_preflight_uri(); // If someone set 'uri' in the 'site' context, then copy it // to the 'process' context (to give it a higher priority // than the 'cli' and 'alias' contexts) and reset our selected // site and @self alias. $uri = drush_get_option('uri'); if ($uri != drush_get_option('uri', $uri, 'site')) { drush_set_option('uri', drush_get_option('uri', $uri, 'site')); _drush_preflight_uri(); } // Create a @self site alias record. drush_sitealias_create_self_alias(); } function _drush_preflight_global_options() { // Debug implies verbose $verbose = drush_get_option('verbose', FALSE); $debug = drush_get_option('debug', FALSE); drush_set_context('DRUSH_VERBOSE', $verbose || $debug); drush_set_context('DRUSH_DEBUG', $debug); drush_set_context('DRUSH_DEBUG_NOTIFY', $verbose && $debug); drush_set_context('DRUSH_SIMULATE', drush_get_option('simulate', FALSE)); // Backend implies affirmative unless negative is explicitly specified drush_set_context('DRUSH_NEGATIVE', drush_get_option('no', FALSE)); drush_set_context('DRUSH_AFFIRMATIVE', drush_get_option(array('yes', 'pipe'), FALSE) || (drush_get_context('DRUSH_BACKEND') && !drush_get_context('DRUSH_NEGATIVE'))); // Pipe implies quiet. drush_set_context('DRUSH_QUIET', drush_get_option(array('quiet', 'pipe'))); drush_set_context('DRUSH_PIPE', drush_get_option('pipe')); // Suppress colored logging if --nocolor option is explicitly given or if // terminal does not support it. $nocolor = (drush_get_option('nocolor', FALSE)); if (!$nocolor) { // Check for colorless terminal. If there is no terminal, then // 'tput colors 2>&1' will return "tput: No value for $TERM and no -T specified", // which is not numeric and therefore will put us in no-color mode. $colors = exec('tput colors 2>&1'); $nocolor = !($colors === FALSE || (is_numeric($colors) && $colors >= 3)); } drush_set_context('DRUSH_NOCOLOR', $nocolor); } /** * Sets up basic environment that controls where Drush looks for files on a * system-wide basis. Important to call for "early" functions that need to * work with unit tests. */ function _drush_preflight_base_environment() { // Copy ETC_PREFIX and SHARE_PREFIX from environment variables if available. // This alters where we check for server-wide config and alias files. // Used by unit test suite to provide a clean environment. if (getenv('ETC_PREFIX')) drush_set_context('ETC_PREFIX', getenv('ETC_PREFIX')); if (getenv('SHARE_PREFIX')) drush_set_context('SHARE_PREFIX', getenv('SHARE_PREFIX')); drush_set_context('DOC_PREFIX', DRUSH_BASE_PATH); if (!file_exists(DRUSH_BASE_PATH . '/README.md') && file_exists(drush_get_context('SHARE_PREFIX', '/usr') . '/share/doc/drush' . '/README.md')) { drush_set_context('DOC_PREFIX', drush_get_context('SHARE_PREFIX', '/usr') . '/share/doc/drush'); } $default_prefix_configuration = drush_is_windows() ? getenv('ALLUSERSPROFILE') . '/Drush' : ''; $default_prefix_commandfile = drush_is_windows() ? getenv('ALLUSERSPROFILE') . '/Drush' : '/usr'; $site_wide_configuration_dir = drush_get_context('ETC_PREFIX', $default_prefix_configuration) . '/etc/drush'; $site_wide_commandfile_dir = drush_get_context('SHARE_PREFIX', $default_prefix_commandfile) . '/share/drush/commands'; drush_set_context('DRUSH_SITE_WIDE_CONFIGURATION', $site_wide_configuration_dir); drush_set_context('DRUSH_SITE_WIDE_COMMANDFILES', $site_wide_commandfile_dir); $server_home = drush_server_home(); if (isset($server_home)) { drush_set_context('DRUSH_PER_USER_CONFIGURATION', $server_home . '/.drush'); } } /* * Set the terminal width, used for wrapping table output. * Normally this is exported using tput in the drush script. * If this is not present we do an additional check using stty here. * On Windows in CMD and PowerShell is this exported using mode con. */ function _drush_preflight_columns() { if (!($columns = getenv('COLUMNS'))) { // Trying to export the columns using stty. exec('stty size 2>&1', $columns_output, $columns_status); if (!$columns_status) $columns = preg_replace('/\d+\s(\d+)/', '$1', $columns_output[0], -1, $columns_count); // If stty fails and Drush us running on Windows are we trying with mode con. if (($columns_status || !$columns_count) && drush_is_windows()) { $columns_output = array(); exec('mode con', $columns_output, $columns_status); if (!$columns_status && is_array($columns_output)) { $columns = (int)preg_replace('/\D/', '', $columns_output[4], -1, $columns_count); } else { drush_log(dt('Drush could not detect the console window width. Set a Windows Environment Variable of COLUMNS to the desired width.'), LogLevel::WARNING); } } // Failling back to default columns value if (empty($columns)) { $columns = 80; } } // If a caller wants to reserve some room to add additional // information to the drush output via post-processing, the // --reserve-margin flag can be used to declare how much // space to leave out. This only affects drush functions // such as drush_print_table() that wrap the output. $columns -= drush_get_option('reserve-margin', 0); drush_set_context('DRUSH_COLUMNS', $columns); } function _drush_preflight_alias_path() { $alias_path =& drush_get_context('ALIAS_PATH'); $default_prefix_configuration = drush_is_windows() ? getenv('ALLUSERSPROFILE') . '/Drush' : ''; $site_wide_configuration_dir = drush_get_context('ETC_PREFIX', $default_prefix_configuration) . '/etc/drush'; $alias_path[] = drush_sitealias_alias_base_directory($site_wide_configuration_dir); $alias_path[] = drush_sitealias_alias_base_directory(dirname(__FILE__) . '/..'); $server_home = drush_server_home(); if (isset($server_home)) { $alias_path[] = drush_sitealias_alias_base_directory($server_home . '/.drush'); } } /* * Set root and uri. */ function _drush_preflight_root_uri() { drush_preflight_root(); _drush_preflight_uri(); } /** * If --uri is provided, set context. */ function _drush_preflight_uri() { $uri = drush_get_option('uri', ''); drush_set_context('DRUSH_SELECTED_URI', $uri); } function _drush_find_commandfiles_drush() { // Core commands shipping with Drush $searchpath[] = dirname(__FILE__) . '/../commands/'; // User commands, specified by 'include' option $include = drush_get_context('DRUSH_INCLUDE', array()); foreach ($include as $path) { if (is_dir($path)) { drush_log('Include ' . $path, LogLevel::NOTICE); $searchpath[] = $path; } } if (!drush_get_option('local')) { // System commands, residing in $SHARE_PREFIX/share/drush/commands $share_path = drush_get_context('DRUSH_SITE_WIDE_COMMANDFILES'); if (is_dir($share_path)) { $searchpath[] = $share_path; } // User commands, residing in ~/.drush $per_user_config_dir = drush_get_context('DRUSH_PER_USER_CONFIGURATION'); if (!empty($per_user_config_dir)) { $searchpath[] = $per_user_config_dir; } } // @todo the zero parameter is a bit weird here. It's $phase. _drush_add_commandfiles($searchpath, 0); // Also discover Drush's own annotation commands. $discovery = annotationcommand_adapter_get_discovery(); $annotation_commandfiles = $discovery->discover(DRUSH_BASE_PATH . '/lib/Drush', '\Drush'); // And, finally, search for commandfiles in the $searchpath $searchpath = array_map( function ($item) { if (strtolower(basename($item)) == 'commands') { return dirname($item); } return $item; }, $searchpath ); $global_drush_extensions = $discovery->discover($searchpath, '\Drush'); $annotation_commandfiles += $global_drush_extensions; drush_set_context('DRUSH_ANNOTATED_COMMANDFILES', $annotation_commandfiles); } /** * Handle any command preprocessing that may need to be done, including * potentially redispatching the command immediately (e.g. for remote * commands). * * @return * TRUE if the command was handled remotely. */ function drush_preflight_command_dispatch() { $interactive = drush_get_option('interactive', FALSE); // The command will be executed remotely if the --remote-host flag // is set; note that if a site alias is provided on the command line, // and the site alias references a remote server, then the --remote-host // option will be set when the site alias is processed. // @see drush_sitealias_check_arg_and_site_set and _drush_sitealias_set_context_by_name $remote_host = drush_get_option('remote-host'); $site_list = drush_get_option('site-list'); // Get the command early so that we can allow commands to directly handle remote aliases if they wish $command = drush_parse_command(); drush_command_default_options($command); // If the command sets the 'strict-option-handling' flag, then we will remove // any cli options that appear after the command name from the 'cli' context. // The cli options that appear before the command name are stored in the // 'DRUSH_GLOBAL_CLI_OPTIONS' context, so we will just overwrite the cli context // with this, after doing the neccessary fixup from short-form to long-form options. // After we do that, we put back any local drush options identified by $command['options']. if (is_array($command) && !empty($command['strict-option-handling'])) { $cli_options = drush_get_context('DRUSH_GLOBAL_CLI_OPTIONS', array()); // Now we are going to sort out any options that exist in $command['options']; // we will remove these from DRUSH_COMMAND_ARGS and put them back into the // cli options. $cli_context = drush_get_context('cli'); $remove_from_command_args = array(); foreach ($command['options'] as $option => $info) { if (array_key_exists($option, $cli_context)) { $cli_options[$option] = $cli_context[$option]; $remove_from_command_args[$option] = $option; } } if (!empty($remove_from_command_args)) { $drush_command_args = array(); foreach (drush_get_context('DRUSH_COMMAND_ARGS') as $arg) { if (!_drush_should_remove_command_arg($arg, $remove_from_command_args)) { $drush_command_args[] = $arg; } } drush_set_context('DRUSH_COMMAND_ARGS', $drush_command_args); } drush_expand_short_form_options($cli_options); drush_set_context('cli', $cli_options); _drush_preflight_global_options(); } $args = drush_get_arguments(); $command_name = array_shift($args); $root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); $local_drush = drush_get_option('drush-script'); if (empty($local_drush) && !empty($root)) { $local_drush = find_wrapper_or_launcher($root); } $is_local = drush_get_option('local'); $values = NULL; if (!empty($root) && !empty($local_drush) && empty($is_local)) { if (!drush_is_absolute_path($local_drush)) { $local_drush = $root . DIRECTORY_SEPARATOR . $local_drush; } $local_drush = realpath($local_drush); $this_drush = drush_find_drush(); // If there is a local Drush selected, and it is not the // same Drush that is currently running, redispatch to it. // We assume that if the current Drush is nested inside // the current Drupal root (or, more specifically, the // current Drupal root's parent), then it is a site-local Drush. // We avoid redispatching in that instance to prevent an // infinite loop. if (file_exists($local_drush) && !drush_is_nested_directory(dirname($root), $this_drush)) { $uri = drush_get_context('DRUSH_SELECTED_URI'); $aditional_options = array( 'root' => $root, ); if (!empty($uri)) { $aditional_options['uri'] = $uri; } // We need to chdir to the Drupal root here, for the // benefit of the Drush wrapper. chdir($root); $values = drush_do_command_redispatch(is_array($command) ? $command : $command_name, $args, NULL, NULL, $local_drush, TRUE, $aditional_options); } } // If the command sets the 'handle-remote-commands' flag, then we will short-circuit // remote command dispatching and site-list command dispatching, and always let // the command handler run on the local machine. if (is_array($command) && !empty($command['handle-remote-commands'])) { return FALSE; } if (isset($remote_host)) { $remote_user = drush_get_option('remote-user'); // Force interactive mode if there is a single remote target. #interactive is added by drush_do_command_redispatch $user_interactive = drush_get_option('interactive'); drush_set_option('interactive', TRUE); $values = drush_do_command_redispatch(is_array($command) ? $command : $command_name, $args, $remote_host, $remote_user, $user_interactive); } // If the --site-list flag is set, then we will execute the specified // command once for every site listed in the site list. if (isset($site_list)) { if (!is_array($site_list)) { $site_list = explode(',', $site_list); } $site_record = array('site-list' => $site_list); $args = drush_get_arguments(); if (!drush_get_context('DRUSH_SIMULATE') && !$interactive && !drush_get_context('DRUSH_AFFIRMATIVE') && !drush_get_context('DRUSH_QUIET')) { drush_print(dt("You are about to execute '!command' non-interactively (--yes forced) on all of the following targets:", array('!command' => implode(" ", $args)))); foreach ($site_list as $one_destination) { drush_print(dt(' !target', array('!target' => $one_destination))); } if (drush_confirm('Continue? ') === FALSE) { drush_user_abort(); return TRUE; } } $command_name = array_shift($args); $multi_options = drush_redispatch_get_options(); $backend_options = array(); if (drush_get_option('pipe') || drush_get_option('interactive')) { $backend_options['interactive'] = TRUE; } if (drush_get_option('no-label', FALSE)) { $backend_options['no-label'] = TRUE; } // If the user specified a format, try to look up the // default list separator for the specified format. // If the user did not specify a different label separator, // then pass in the default as an option, so that the // separator between the items in the list and the site // name will be consistent. $format = drush_get_option('format', FALSE); if ($format && !array_key_exists('label-separator', $multi_options)) { $formatter = drush_load_engine('outputformat', $format); if ($formatter) { $list_separator = $formatter->get_info('list-separator'); if ($list_separator) { $multi_options['label-separator'] = $list_separator; } } } $values = drush_invoke_process($site_record, $command_name, $args, $multi_options, $backend_options); } if (isset($values)) { if (is_array($values) && ($values['error_status'] > 0)) { // Force an error result code. Note that drush_shutdown() will still run. drush_set_context('DRUSH_EXECUTION_COMPLETED', TRUE); exit($values['error_status']); } return TRUE; } return FALSE; } /** * Look for instances of arguments or parameters that * start with "~/". Replace these with "$HOME/". * * Note that this function is called _after_ Drush does * its redispatch checks; tildes are passed through * unmodified on a redispatch, and are only expanded when * a command is handled locally. */ function drush_preflight_tilde_expansion(&$command) { // Skip tilde expansion for commands that use // stict option handling, or those that explicitly // turn it off via $command['tilde-expansion'] = FALSE. if ($command['tilde-expansion'] && !$command['strict-option-handling']) { $cli =& drush_get_context('cli'); $match = '#^~/#'; $replacement = drush_server_home() . '/'; foreach ($cli as $key => $value) { if (is_string($value) && preg_match($match, $value)) { $cli[$key] = preg_replace($match, $replacement, $value); } } $command['arguments'] = array_map(function($value) use($match, $replacement) { return is_string($value) ? preg_replace($match, $replacement, $value) : $value; } , $command['arguments']); } } /** * We set this context to let the shutdown function know we reached the end of drush_main(). * * @see drush_main() */ function drush_postflight() { drush_set_context("DRUSH_EXECUTION_COMPLETED", TRUE); } /** * Shutdown function for use while Drush and Drupal are bootstrapping and to return any * registered errors. * * The shutdown command checks whether certain options are set to reliably * detect and log some common Drupal initialization errors. * * If the command is being executed with the --backend option, the script * will return a json string containing the options and log information * used by the script. * * The command will exit with '1' if it was successfully executed, and the * result of drush_get_error() if it wasn't. */ function drush_shutdown() { // Mysteriously make $user available during sess_write(). Avoids a NOTICE. global $user; if (!drush_get_context('DRUSH_EXECUTION_COMPLETED', FALSE) && !drush_get_context('DRUSH_USER_ABORT', FALSE)) { $php_error_message = ''; if ($error = error_get_last()) { $php_error_message = "\n" . dt('Error: !message in !file, line !line', array('!message' => $error['message'], '!file' => $error['file'], '!line' => $error['line'])); } // We did not reach the end of the drush_main function, // this generally means somewhere in the code a call to exit(), // was made. We catch this, so that we can trigger an error in // those cases. drush_set_error("DRUSH_NOT_COMPLETED", dt("Drush command terminated abnormally due to an unrecoverable error.!message", array('!message' => $php_error_message))); // Attempt to give the user some advice about how to fix the problem _drush_postmortem(); } // @todo Ask the bootstrap object (or maybe dispatch) how far we got. $phase = drush_get_context('DRUSH_BOOTSTRAP_PHASE'); if (drush_get_context('DRUSH_BOOTSTRAPPING')) { switch ($phase) { case DRUSH_BOOTSTRAP_DRUPAL_FULL : ob_end_clean(); _drush_log_drupal_messages(); drush_set_error('DRUSH_DRUPAL_BOOTSTRAP_ERROR'); break; } } if (drush_get_context('DRUSH_BACKEND', FALSE)) { drush_backend_output(); } elseif (drush_get_context('DRUSH_QUIET', FALSE)) { ob_end_clean(); // If we are in pipe mode, emit the compact representation of the command, if available. if (drush_get_context('DRUSH_PIPE')) { drush_pipe_output(); } } // This way drush_return_status() will always be the last shutdown function (unless other shutdown functions register shutdown functions...) // and won't prevent other registered shutdown functions (IE from numerous cron methods) from running by calling exit() before they get a chance. register_shutdown_function('drush_return_status'); } /** * Shutdown function to save code coverage data. */ function drush_coverage_shutdown() { if ($file_name = drush_get_context('DRUSH_CODE_COVERAGE', FALSE)) { $data = xdebug_get_code_coverage(); xdebug_stop_code_coverage(); // If coverage dump file contains anything, merge in the old data before // saving. This happens if the current drush command invoked another drush // command. if (file_exists($file_name) && $content = file_get_contents($file_name)) { $merge_data = unserialize($content); if (is_array($merge_data)) { foreach ($merge_data as $file => $lines) { if (!isset($data[$file])) { $data[$file] = $lines; } else { foreach ($lines as $num => $executed) { if (!isset($data[$file][$num])) { $data[$file][$num] = $executed; } else { $data[$file][$num] = ($executed == 1 ? $executed : $data[$file][$num]); } } } } } } file_put_contents($file_name, serialize($data)); } } function drush_return_status() { // If a specific exit code was set, then use it. $exit_code = drush_get_context('DRUSH_EXIT_CODE'); if (empty($exit_code)) { $exit_code = (drush_get_error()) ? DRUSH_FRAMEWORK_ERROR : DRUSH_SUCCESS; } exit($exit_code); } $drupal_root, 'uri' => $uri)); } } } /** * Given a list of alias records, shorten the name used if possible */ function drush_sitealias_simplify_names($site_list) { $result = array(); foreach ($site_list as $original_name => $alias_record) { $adjusted_name = $alias_record['#name']; $hashpos = strpos($original_name, '#'); if ($hashpos !== FALSE) { $adjusted_name = substr($original_name, $hashpos); if (array_key_exists('remote-host', $alias_record)) { $adjusted_name = $alias_record['remote-host'] . $adjusted_name; } } $result[$adjusted_name] = $alias_record; } return $result; } /** * Given an array of site specifications, resolve each one in turn and * return an array of alias records. If you only want a single record, * it is preferable to simply call drush_sitealias_get_record() directly. * * @param $site_specifications * One of: * A comma-separated list of site specifications: '@site1,@site2' * An array of site specifications: array('@site1','@site2') * An array of alias records: * array( * 'site1' => array('root' => ...), * 'site2' => array('root' => ...) * ) * An array of site specifications. * @see drush_sitealias_get_record() for the format of site specifications. * @return * An array of alias records */ function drush_sitealias_resolve_sitespecs($site_specifications, $alias_path_context = NULL) { $result_list = array(); $not_found = array(); if (!is_array($site_specifications)) { $site_specifications = explode(',', $site_specifications); } if (!empty($site_specifications)) { foreach ($site_specifications as $site) { if (is_array($site)) { $result_list[] = $site; } else { $alias_record = drush_sitealias_get_record($site, $alias_path_context); if (!$alias_record) { $not_found[] = $site; } else { $result_list = array_merge($result_list, drush_sitealias_resolve_sitelist($alias_record)); } } } } return array($result_list, $not_found); } /** * Returns TRUE if $alias is a valid format for an alias name. * * Mirrors the allowed formats shown below for drush_sitealias_get_record. */ function drush_sitealias_valid_alias_format($alias) { return ( (strpos($alias, ',') !== false) || ((strpos($alias, '@') === FALSE ? 0 : 1) + (strpos($alias, '/') === FALSE ? 0 : 1) + (strpos($alias, '#') === FALSE ? 0 : 1) >= 2) || ($alias{0} == '#') || ($alias{0} == '@') ); } /** * Get a site alias record given an alias name or site specification. * * If it is the name of a site alias, return the alias record from * the site aliases array. * * If it is the name of a folder in the 'sites' folder, construct * an alias record from values stored in settings.php. * * If it is a site specification, construct an alias record from the * values in the specification. * * Site specifications come in several forms: * - /path/to/drupal#sitename * - user@server/path/to/drupal#sitename * - user@server/path/to/drupal (sitename == server) * - user@server#sitename (only if $option['r'] set in some drushrc file on server) * - #sitename (only if $option['r'] already set, and 'sitename' is a folder in $option['r']/sites) * - sitename (only if $option['r'] already set, and 'sitename' is a folder in $option['r']/sites) * * Note that in the case of the first four forms, it is also possible * to add additional site variable to the specification using uri query * syntax. For example: * * user@server/path/to/drupal?db-url=...#sitename * * @param alias * An alias name or site specification * @return array * An alias record, or empty if none found. */ function drush_sitealias_get_record($alias, $alias_context = NULL) { // Check to see if the alias contains commas. If it does, then // we will go ahead and make a site list record $alias_record = array(); if (strpos($alias, ',') !== false) { // TODO: If the site list contains any site lists, or site // search paths, then we should expand those and merge them // into this list longhand. $alias_record['site-list'] = explode(',', $alias); } else { $alias_record = _drush_sitealias_get_record($alias, $alias_context); } if (!empty($alias_record)) { if (array_key_exists('#name', $alias_record)) { if ($alias_record['#name'] == 'self') { $path = drush_sitealias_local_site_path($alias_record); if ($path) { $cached_alias_record = drush_sitealias_lookup_alias_by_path($path); // Don't overrite keys which have already been negotiated. unset($cached_alias_record['#name'], $cached_alias_record['root'], $cached_alias_record['uri']); $alias_record = array_merge($alias_record, $cached_alias_record); } } } else { $alias_record['#name'] = drush_sitealias_uri_to_site_dir($alias); } } return $alias_record; } /** * This is a continuation of drush_sitealias_get_record, above. It is * not intended to be called directly. */ function _drush_sitealias_get_record($alias, $alias_context = NULL) { $alias_record = array(); // Before we do anything else, load $alias if it needs to be loaded _drush_sitealias_load_alias($alias, $alias_context); // Check to see if the provided parameter is in fact a defined alias. $all_site_aliases =& drush_get_context('site-aliases'); if (array_key_exists($alias, $all_site_aliases)) { $alias_record = $all_site_aliases[$alias]; } // If the parameter is not an alias, then it is some form of // site specification (or it is nothing at all) else { if (isset($alias)) { // Cases 1.) - 4.): // We will check for a site specification if the alias has at least // two characters from the set '@', '/', '#'. if ((strpos($alias, '@') === FALSE ? 0 : 1) + ((strpos($alias, '/') === FALSE && strpos($alias, '\\') === FALSE) ? 0 : 1) + (strpos($alias, '#') === FALSE ? 0 : 1) >= 2) { if ((substr($alias,0,7) != 'http://') && !drush_is_absolute_path($alias)) { // Add on a scheme so that "user:pass@server" will always parse correctly $parsed = parse_url('http://' . $alias); } else if (drush_is_windows() && drush_is_absolute_path($alias)) { // On windows if alias begins with a filesystem path we must add file:// scheme to make it parse correcly $parsed = parse_url('file:///' . $alias); } else { $parsed = parse_url($alias); } // Copy various parts of the parsed URL into the appropriate records of the alias record foreach (array('user' => 'remote-user', 'pass' => 'remote-pass', 'host' => 'remote-host', 'fragment' => 'uri', 'path' => 'root') as $url_key => $option_key) { if (array_key_exists($url_key, $parsed)) { _drush_sitealias_set_record_element($alias_record, $option_key, $parsed[$url_key]); } } // If the site specification has a query, also set the query items // in the alias record. This allows passing db_url as part of the // site specification, for example. if (array_key_exists('query', $parsed)) { foreach (explode('&', $parsed['query']) as $query_arg) { $query_components = explode('=', $query_arg); _drush_sitealias_set_record_element($alias_record, urldecode($query_components[0]), urldecode($query_components[1])); } } // Case 3.): If the URL contains a 'host' portion but no fragment, then set the uri to the host // Note: We presume that 'server' is the best default for case 3; without this code, the default would // be whatever is set in $options['l'] on the target machine's drushrc.php settings file. if (array_key_exists('host', $parsed) && !array_key_exists('fragment', $parsed)) { $alias_record['uri'] = $parsed['host']; } // Special checking: relative aliases embedded in a path $relative_alias_pos = strpos($alias_record['root'], '/@'); if ($relative_alias_pos !== FALSE) { // Special checking: /path/@sites $base = substr($alias_record['root'], 0, $relative_alias_pos); $relative_alias = substr($alias_record['root'], $relative_alias_pos + 1); if (drush_valid_root($base) || ($relative_alias == '@sites')) { drush_sitealias_create_sites_alias($base); $alias_record = drush_sitealias_get_record($relative_alias); } else { $alias_record = array(); } } } else { // Case 5.) and 6.): // If the alias is the name of a folder in the 'sites' directory, // then use it as a local site specification. $alias_record = _drush_sitealias_find_record_for_local_site($alias); } } } if (!empty($alias_record)) { if (!isset($alias_record['remote']) && !isset($alias_record['#loaded-config'])) { if (array_key_exists('root', $alias_record)) { drush_sitealias_add_to_alias_path($alias_record['root'] . '/drush'); drush_sitealias_add_to_alias_path($alias_record['root'] . '/sites/all/drush'); } // TODO: We should probably remove this feature, and put it back // in, but in different places (e.g. site selection, sql-sync + rsync // parameters, etc.) $alias_site_dir = drush_sitealias_local_site_path($alias_record); if (isset($alias_site_dir)) { // Add the sites folder of this site to the alias search path list drush_sitealias_add_to_alias_path($alias_site_dir); } if (isset($alias_record['config']) && file_exists($alias_record['config'])) { drush_load_config_file('site', $alias_record['config']); $alias_record['#loaded-config'] = TRUE; } unset($alias_record['config']); } // Add the static defaults _drush_sitealias_add_static_defaults($alias_record); // Cache the result with all of its calculated values $all_site_aliases[$alias] = $alias_record; } return $alias_record; } /** * Add a path to the array of paths where alias files are searched for. * * @param $add_path * A path to add to the search path (or NULL to not add any). * Once added, the new path will remain available until drush * exits. * @return * An array of paths containing all values added so far */ function drush_sitealias_add_to_alias_path($add_path) { static $site_paths = array(); if ($add_path != NULL) { if (!is_array($add_path)) { $add_path = explode(PATH_SEPARATOR, $add_path); } // Normalize path to make sure we don't add the same path twice on // windows due to different spelling. e.g. c:\tmp and c:/tmp foreach($add_path as &$path) { $path = drush_normalize_path($path); } $site_paths = array_unique(array_merge($site_paths, $add_path)); } return $site_paths; } /** * Return the array of paths where alias files are searched for. * * @param $alias_path_context * If the alias being looked up is part of a relative alias, * the alias path context specifies the context of the primary * alias the new alias is rooted from. Alias files stored in * the sites folder of this context, or inside the context itself * takes priority over any other search path that might define * a similarly-named alias. In this way, multiple sites can define * a '@peer' alias. * @return * An array of paths */ function drush_sitealias_alias_path($alias_path_context = NULL) { $context_path = array(); if (isset($alias_path_context)) { $context_path = array(drush_sitealias_local_site_path($alias_path_context)); } // We get the current list of site paths by adding NULL // (nothing) to the path list, which is a no-op $site_paths = drush_sitealias_add_to_alias_path(NULL); // If the user defined the root of a drupal site, then also // look for alias files in /drush and /sites/all/drush. $drupal_root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); if (!empty($drupal_root)) { $site_paths[] = drush_sitealias_alias_base_directory($drupal_root . '/../drush'); $site_paths[] = drush_sitealias_alias_base_directory($drupal_root . '/drush'); $site_paths[] = drush_sitealias_alias_base_directory($drupal_root . '/sites/all/drush'); $uri = drush_get_context('DRUSH_SELECTED_URI'); if (empty($uri)) { $uri = 'default'; } $site_dir = drush_sitealias_uri_to_site_dir($uri, $drupal_root); if ($site_dir) { $site_paths[] = drush_sitealias_alias_base_directory("$drupal_root/sites/$site_dir"); } } $alias_path = (array) drush_get_context('ALIAS_PATH', array()); return array_unique(array_merge($context_path, $alias_path, $site_paths)); } /** * If there is a directory 'site-aliases' in the specified search location, * then search ONLY in that directory for aliases. Otherwise, search * anywhere inside the specified directory for aliases. */ function drush_sitealias_alias_base_directory($dir) { $potential_location = $dir . '/site-aliases'; if (is_dir($potential_location)) { return $potential_location; } return $dir; } /** * Return the full path to the site directory of the * given alias record. * * @param $alias_record * The alias record * @return * The path to the site directory of the associated * alias record, or NULL if the record is not a local site. */ function drush_sitealias_local_site_path($alias_record) { $result = NULL; if (isset($alias_record['root']) && !isset($alias_record['remote-host'])) { if (isset($alias_record['uri'])) { $uri = $alias_record['uri']; $uri = preg_replace('#^[^:]*://#', '', $uri); while (!$result && !empty($uri)) { if (file_exists($alias_record['root'] . '/sites/sites.php')) { $sites = array(); include($alias_record['root'] . '/sites/sites.php'); if (array_key_exists($uri, $sites)) { $result = $alias_record['root'] . '/sites/' . $sites[$uri]; } } if (!$result) { $result = ($alias_record['root'] . '/sites/' . drush_sitealias_uri_to_site_dir($uri, drush_sitealias_get_root($alias_record))); } $result = realpath($result); $uri = preg_replace('#^[^.]*\.*#', '', $uri); } } if (!$result) { $result = realpath($alias_record['root'] . '/sites/default'); } } return $result; } /** * Check and see if an alias definition for $alias is available. * If it is, load it into the list of aliases cached in the * 'site-aliases' context. * * @param $alias * The name of the alias to load in ordinary form ('@name') * @param $alias_path_context * When looking up a relative alias, the alias path context is * the primary alias that we will start our search from. */ function _drush_sitealias_load_alias($alias, $alias_path_context = NULL) { $all_site_aliases = drush_get_context('site-aliases'); $result = array(); // Only aliases--those named entities that begin with '@'--can be loaded this way. // We also skip any alias that has already been loaded. if ((substr($alias,0,1) == '@') && !array_key_exists($alias,$all_site_aliases)) { $aliasname = substr($alias,1); $result = _drush_sitealias_find_and_load_alias($aliasname, $alias_path_context); if (!empty($result)) { $alias_options = array('site-aliases' => array($aliasname => $result)); _drush_sitealias_add_inherited_values($alias_options['site-aliases']); drush_set_config_special_contexts($alias_options); if (array_key_exists('#file', $result)) { drush_log(dt('Loaded alias !alias from file !file', array('!alias' => $alias, '!file' => $result['#file']))); } } } return $result; } /** * Load every alias file that can be found anywhere in the * alias search path. */ function drush_sitealias_load_all($resolve_parent = TRUE) { $result = _drush_sitealias_find_and_load_all_aliases(); if (!empty($result) && ($resolve_parent == TRUE)) { // If any aliases were returned, then check for // inheritance and then store the aliases into the // alias cache _drush_sitealias_add_inherited_values($result); $alias_options = array('site-aliases' => $result); drush_set_config_special_contexts($alias_options); } } /** * Worker function called by _drush_sitealias_load_alias and * drush_sitealias_load_all. Traverses the alias search path * and finds the specified alias record. * * @return * An array of $kay => $value pair of alias names and alias records * loaded. */ function _drush_sitealias_find_and_load_all_aliases() { $result = array(); $drush_alias_files = _drush_sitealias_find_alias_files(); drush_set_context('drush-alias-files', $drush_alias_files); // For every file that matches, check inside it for // an alias with a matching name. foreach ($drush_alias_files as $filename) { if (file_exists($filename)) { $aliases = $options = array(); // silently ignore files we can't include if ((@include $filename) === FALSE) { drush_log(dt('Cannot open alias file "!alias", ignoring.', array('!alias' => realpath($filename))), LogLevel::BOOTSTRAP); continue; } unset($options['site-aliases']); // maybe unnecessary // If $aliases are not set, but $options are, then define one alias named // after the first word of the file, before '.alias.drushrc.php. if (empty($aliases) && !empty($options)) { $this_alias_name = substr(basename($filename),0,strpos(basename($filename),'.')); $aliases[$this_alias_name] = $options; $options = array(); } // If this is a group alias file, then make an // implicit alias from the group name that contains // a site-list of all of the aliases in the file $group_name = ''; if (substr($filename, -20) == ".aliases.drushrc.php") { $group_name = basename($filename,".aliases.drushrc.php"); if (!empty($aliases) && !array_key_exists($group_name, $aliases)) { $alias_names = array(); foreach (array_keys($aliases) as $one_alias) { $alias_names[] = "@$group_name.$one_alias"; $aliases["$group_name.$one_alias"] = $aliases[$one_alias]; unset($aliases[$one_alias]); } $aliases[$group_name] = array('site-list' => implode(',', $alias_names)); } } if (!empty($aliases)) { if (!empty($options)) { foreach ($aliases as $name => $value) { $aliases[$name] = array_merge($options, $value); } $options = array(); } foreach ($aliases as $name => $value) { _drush_sitealias_initialize_alias_record($aliases[$name]); $aliases[$name]['#name'] = $name; $aliases[$name]['#file'] = $filename; } $result = _sitealias_array_merge($result, $aliases); // If we found at least one alias from this file // then record it in the drush-alias-files context. $drush_alias_files = drush_get_context('drush-alias-files'); if (!in_array($filename, $drush_alias_files)) { $drush_alias_files[] = $filename; } drush_set_context('drush-alias-files', $drush_alias_files); } } } return $result; } /** * Function to find all alias files that might contain aliases * that match the requested alias name. */ function _drush_sitealias_find_alias_files($aliasname = NULL, $alias_path_context = NULL) { $alias_files_to_consider = array(); // The alias path is a list of folders to search for alias settings files $alias_path = drush_sitealias_alias_path($alias_path_context); // $alias_files contains a list of filename patterns // to search for. We will find any matching file in // any folder in the alias path. The directory scan // is not deep, though; only files immediately in the // search path are considered. $alias_files = array('/.*aliases\.drush(' . DRUSH_MAJOR_VERSION . '|)rc\.php$/'); if ($aliasname == NULL) { $alias_files[] = '/.*\.alias\.drush(' . DRUSH_MAJOR_VERSION . '|)rc\.php$/'; } else { $alias_files[] = '/' . preg_quote($aliasname, '/') . '\.alias\.drush(' . DRUSH_MAJOR_VERSION . '|)rc\.php$/'; } // Do not scan into the files directory. $blacklist = array_merge(array('files'), drush_filename_blacklist()); // Search each path in turn. foreach ($alias_path as $path) { // Find all of the matching files in this location foreach ($alias_files as $file_pattern_to_search_for) { drush_log(dt('Scanning into @path for @pattern', array('@path' => $path, '@pattern' => $file_pattern_to_search_for)), LogLevel::DEBUG_NOTIFY); $alias_files_to_consider = array_merge($alias_files_to_consider, array_keys(drush_scan_directory($path, $file_pattern_to_search_for, $blacklist, 0, TRUE))); } } return $alias_files_to_consider; } /** * Traverses the alias search path and finds the specified alias record. * * @param $aliasname * The name of the alias without the leading '@' (i.e. '#name') * or NULL to load every alias found in every alias file. * @param $alias_path_context * When looking up a relative alias, the alias path context is * the primary alias that we will start our search from. * @return * An empty array if nothing was loaded. If $aliasname is * not null, then the array returned is the alias record for * $aliasname. If $aliasname is NULL, then the array returned * is a $kay => $value pair of alias names and alias records * loaded. */ function _drush_sitealias_find_and_load_alias($aliasname, $alias_path_context = NULL) { // Special checking for '@sites' alias if ($aliasname == 'sites') { $drupal_root = NULL; if ($alias_path_context != null) { if (array_key_exists('root', $alias_path_context) && !array_key_exists('remote-host', $alias_path_context)) { $drupal_root = $alias_path_context['root']; } } else { $drupal_root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); } if (isset($drupal_root) && !is_array($drupal_root)) { drush_sitealias_create_sites_alias($drupal_root); } } $alias_files_to_consider = _drush_sitealias_find_alias_files($aliasname, $alias_path_context); return _drush_sitealias_find_and_load_alias_from_file($aliasname, $alias_files_to_consider); } function _drush_sitealias_find_and_load_alias_from_file($aliasname, $alias_files_to_consider) { $result = array(); $result_names = array(); // For every file that matches, check inside it for // an alias with a matching name. $recorded_files = array(); foreach ($alias_files_to_consider as $filename) { if (file_exists($filename)) { $aliases = $options = array(); // silently ignore files we can't include if ((@include $filename) === FALSE) { drush_log(dt('Cannot open alias file "!alias", ignoring.', array('!alias' => realpath($filename))), LogLevel::BOOTSTRAP); continue; } unset($options['site-aliases']); // maybe unnecessary // If $aliases are not set, but $options are, then define one alias named // after the first word of the file, before '.alias.drushrc.php. if (empty($aliases) && !empty($options)) { $this_alias_name = substr(basename($filename),0,strpos(basename($filename),'.')); $aliases[$this_alias_name] = $options; $options = array(); } // If this is a group alias file, then make an // implicit alias from the group name that contains // a site-list of all of the aliases in the file $group_prefix = ''; if (substr($filename, -20) == ".aliases.drushrc.php") { $group_name = basename($filename,".aliases.drushrc.php"); $group_prefix = $group_name . '.'; if (!empty($aliases) && !array_key_exists($group_name, $aliases)) { $alias_names = array(); foreach (array_keys($aliases) as $one_alias) { $alias_names[] = "@$group_name.$one_alias"; $aliases[$one_alias]['#name'] = "$group_name.$one_alias"; $aliases[$one_alias]['#group'] = $group_name; $aliases["$group_name.$one_alias"] = $aliases[$one_alias]; $aliases[$one_alias]["#hidden"] = TRUE; } $aliases[$group_name] = array('site-list' => implode(',', $alias_names), '#group' => $group_name, '#name' => $group_name); } } // Store only the named alias into the alias cache if ((isset($aliases)) && !empty($aliasname) && array_key_exists($aliasname, $aliases)) { drush_set_config_special_contexts($options); // maybe unnecessary $one_result = array_merge($options, $aliases[$aliasname]); $one_result['#file'] = $filename; if (!array_key_exists('#name', $one_result)) { $one_result['#name'] = $aliasname; } _drush_sitealias_initialize_alias_record($one_result); // If the alias name is exactly the same as a previous match, then // merge the two records together if (!empty($result) && ($result['#name'] == $one_result['#name'])) { $result = _sitealias_array_merge($result, $one_result); } // Add the name of the found record to the list of results else { $result_names[] = "@" . $one_result['#name']; $result = $one_result; } } } } // If there are multiple matches, then return a list of results. if (count($result_names) > 1) { $result = array('site-list' => $result_names); } return $result; } /** * Merges two site aliases. * * array_merge_recursive is too much; we only want to run * array_merge on the common top-level keys of the array. * * @param array $site_alias_a * A site alias array. * @param array $site_alias_b * A site alias array. * @return * A site alias array where the keys from $site_alias_a are overwritten by the * keys from $site_alias_b. */ function _sitealias_array_merge($site_alias_a, $site_alias_b) { $result = $site_alias_a; foreach($site_alias_b as $key => $value) { if (is_array($value) && array_key_exists($key, $result)) { $result[$key] = array_merge($result[$key], $value); } else { $result[$key] = $value; } } return $result; } /** * Check to see if there is a 'parent' item in the alias; if there is, * then load the parent alias record and overlay the entries in the * current alias record on top of the items from the parent record. * * @param $aliases * An array of alias records that are modified in-place. */ function _drush_sitealias_add_inherited_values(&$aliases) { foreach ($aliases as $alias_name => $alias_value) { // Prevent circular references from causing an infinite loop _drush_sitealias_cache_alias("@$alias_name", array()); _drush_sitealias_add_inherited_values_to_record($alias_value); $aliases[$alias_name] = $alias_value; } } function _drush_sitealias_add_inherited_values_to_record(&$alias_value) { drush_command_invoke_all_ref('drush_sitealias_alter', $alias_value); if (isset($alias_value['parent'])) { drush_log(dt("Using deprecated 'parent' element '!parent' in '!name'.", array('!parent' => $alias_value['parent'], '!name' => $alias_value['#name'])), LogLevel::DEBUG); // Fetch and merge in each parent foreach (explode(',', $alias_value['parent']) as $parent) { $parent_record = drush_sitealias_get_record($parent); unset($parent_record['#name']); unset($parent_record['#file']); unset($parent_record['#hidden']); $array_based_keys = array_merge(drush_get_special_keys(), array('path-aliases')); foreach ($array_based_keys as $array_based_key) { if (isset($alias_value[$array_based_key]) && isset($parent_record[$array_based_key])) { $alias_value[$array_based_key] = array_merge($parent_record[$array_based_key], $alias_value[$array_based_key]); } } $alias_value = array_merge($parent_record, $alias_value); } } unset($alias_value['parent']); } /** * Add an empty record for the specified alias name * * @param $alias_name * The name of the alias, including the leading "@" */ function _drush_sitealias_cache_alias($alias_name, $alias_record) { $cache =& drush_get_context('site-aliases'); // If the alias already exists in the cache, then merge // the new alias with the existing alias if (array_key_exists($alias_name, $cache)) { $alias_record = array_merge($cache[$alias_name], $alias_record); } if (!isset($alias_record['#name'])) { $alias_record['#name'] = trim($alias_name, '@'); } $cache[$alias_name] = $alias_record; // If the alias record points at a local site, make sure // that /drush, /sites/all/drush and the site folder for that site // are added to the alias path, so that other alias files // stored in those locations become searchable. if (!array_key_exists('remote-host', $alias_record) && !empty($alias_record['root'])) { drush_sitealias_add_to_alias_path($alias_record['root'] . '/drush'); drush_sitealias_add_to_alias_path($alias_record['root'] . '/sites/all/drush'); $site_dir = drush_sitealias_local_site_path($alias_record); if (isset($site_dir)) { drush_sitealias_add_to_alias_path($site_dir); } } } /** * If the alias record does not contain a 'databases' or 'db-url' * entry, then use backend invoke to look up the settings value * from the remote or local site. The 'db_url' form is preferred; * nothing is done if 'db_url' is not available (e.g. on a D7 site) * * @param $alias_record * The full alias record to populate with database settings */ function drush_sitealias_add_db_url(&$alias_record) { if (!isset($alias_record['db-url']) && !isset($alias_record['databases']) && !isset($alias_record['site-list'])) { drush_sitealias_add_db_settings($alias_record); } if (!isset($alias_record['db-url']) && isset($alias_record['databases'])) { $alias_record['db-url'] = drush_sitealias_convert_databases_to_db_url($alias_record['databases']); } } /** * Drush still accepts --db-url format database specifications as * cli parameters; it is therefore useful to be able to convert * from a database record back to a db-url sometimes. */ function drush_sitealias_convert_db_spec_to_db_url($db_spec) { $result = urlencode($db_spec["driver"]) . "://"; if (isset($db_spec["username"])) { $result .= urlencode($db_spec["username"]); if (isset($db_spec["password"])) { $result .= ":" . urlencode($db_spec["password"]); } $result .= "@"; } // Host is required, unless this is an sqlite db. if (isset($db_spec["host"])) { $result .= urlencode($db_spec["host"]); if (isset($db_spec["port"])) { $result .= ":" . urlencode($db_spec["port"]); } $result .= '/' . urlencode($db_spec["database"]); } else { // URL-encode the database, but convert slashes // back to their original form for readability. // This portion is the "path" of the URL, so it may // contain slashes. This is important for sqlite. $result .= str_replace("%2F", "/", urlencode(ltrim($db_spec["database"], '/'))); } return $result; } /** * Create a db-url from the databases record. */ function drush_sitealias_convert_databases_to_db_url($databases) { if ((count($databases) == 1) && isset($databases['default'])) { $result = drush_sitealias_convert_db_spec_to_db_url($databases['default']['default']); } else { foreach ($databases as $key => $db_info) { $result[$key] = drush_sitealias_convert_db_spec_to_db_url($db_info['default']); } } return $result; } /** * Return the databases record from the alias record * * @param $alias_record * A record returned from drush_sitealias_get_record * @returns * A databases record (always in D7 format) or NULL * if the databases record could not be found. */ function sitealias_get_databases_from_record(&$alias_record) { $altered_record = drush_sitealias_add_db_settings($alias_record); return array_key_exists('databases', $alias_record) ? $alias_record['databases'] : NULL; } /** * Return the $db_spec record for the database associated with * the provided alias record. @see drush_sitealias_add_db_settings(), * which will be used to first add the database information to the * alias records, invoking sql-conf to look them up if necessary. * * The options 'database' and 'target' are used to specify which * specific database should be fetched from the database record; * they may appear in the alias definition, or may be taken from the * command line options. The values 'default' and 'default' are * used if these options are not specified in either location. * * Note that in the context of sql-sync, the site alias record will * be taken from one of the source or target aliases * (e.g. `drush sql-sync @source @target`), which will be overlayed with * any options that begin with 'source-' or 'target-', respectively. * Therefore, the commandline options 'source-database' and 'source-target' * (or 'target-database' and 'source-target') may also affect the operation * of this function. */ function drush_sitealias_get_db_spec(&$alias_record, $default_to_self = FALSE, $prefix = '') { $db_spec = NULL; $databases = sitealias_get_databases_from_record($alias_record); if (isset($databases) && !empty($databases)) { $database = drush_sitealias_get_option($alias_record, 'database', 'default', $prefix); $target = drush_sitealias_get_option($alias_record, 'target', 'default', $prefix); if (array_key_exists($database, $databases) && array_key_exists($target, $databases[$database])) { $db_spec = $databases[$database][$target]; } } elseif ($default_to_self) { $db_spec = _drush_sql_get_db_spec(); } if (isset($db_spec)) { $remote_host = drush_sitealias_get_option($alias_record, 'remote-host', NULL, $prefix); if (!drush_is_local_host($remote_host)) { $db_spec['remote-host'] = $remote_host; $db_spec['port'] = drush_sitealias_get_option($alias_record, 'remote-port', (isset($db_spec['port']) ? $db_spec['port'] : NULL), $prefix); } } return $db_spec; } /** * If the alias record does not contain a 'databases' or 'db-url' * entry, then use backend invoke to look up the settings value * from the remote or local site. The 'databases' form is * preferred; 'db_url' will be converted to 'databases' if necessary. * * @param $alias_record * The full alias record to populate with database settings */ function drush_sitealias_add_db_settings(&$alias_record) { $altered_record = FALSE; if (isset($alias_record['root'])) { // If the alias record does not have a defined 'databases' entry, // then we'll need to look one up if (!isset($alias_record['db-url']) && !isset($alias_record['databases']) && !isset($alias_record['site-list'])) { $values = drush_invoke_process($alias_record, "sql-conf", array(), array('all' => TRUE), array('integrate' => FALSE, 'override-simulated' => TRUE)); if (is_array($values) && ($values['error_status'] == 0)) { $altered_record = TRUE; // If there are any special settings in the '@self' record returned by drush_invoke_process, // then add those into our altered record as well if (array_key_exists('self', $values)) { $alias_record = array_merge($values['self'], $alias_record); } drush_sitealias_cache_db_settings($alias_record, $values['object']); } } } return $altered_record; } function drush_sitealias_cache_db_settings(&$alias_record, $databases) { if (!empty($databases)) { $alias_record['databases'] = $databases; } // If the name is set, then re-cache the record after we fetch the databases if (array_key_exists('#name', $alias_record)) { $all_site_aliases =& drush_get_context('site-aliases'); $all_site_aliases['@' . $alias_record['#name']] = $alias_record; // Check and see if this record is a copy of 'self' if (($alias_record['#name'] != 'self') && array_key_exists('@self', $all_site_aliases) && array_key_exists('#name', $all_site_aliases['@self']) && ($all_site_aliases['@self']['#name'] == $alias_record['#name'])) { $all_site_aliases['@self'] = $alias_record; } } } /** * Check to see if we have already bootstrapped to a site. */ function drush_sitealias_is_bootstrapped_site($alias_record) { if (!isset($alias_record['remote-host']) && array_key_exists('root', $alias_record)) { $self_record = drush_sitealias_get_record("@self"); if (empty($self_record) || !array_key_exists('root', $self_record)) { // TODO: If we have not bootstrapped to a site yet, we could // perhaps bootstrap to $alias_record here. return FALSE; } elseif(($alias_record['root'] == $self_record['root']) && ($alias_record['uri'] == $self_record['uri'])) { return TRUE; } } return FALSE; } /** * Determines whether a given site alias is for a remote site. * * @param string $alias * An alias name or site specification. * * @return bool * Returns TRUE if the alias refers to a remote site, FALSE if it does not, or NULL is unsure. */ function drush_sitealias_is_remote_site($alias) { if (is_array($alias) && !empty($alias['remote-host'])) { return TRUE; } if (!is_string($alias) || !strlen($alias)) { return NULL; } $site_record = drush_sitealias_get_record($alias); if ($site_record) { if (!empty($site_record['remote-host'])) { return TRUE; } else { return FALSE; } } else { drush_set_error('Unrecognized site alias.'); } } /** * Get the name of the current bootstrapped site */ function drush_sitealias_bootstrapped_site_name() { $site_name = NULL; $self_record = drush_sitealias_get_record('@self'); if (array_key_exists('#name', $self_record)) { $site_name = $self_record['#name']; } if (!isset($site_name) || ($site_name == '@self')) { $drupal_root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); if (isset($drupal_root)) { $drupal_uri = drush_get_context('DRUSH_SELECTED_URI', 'default'); $drupal_uri = str_replace('http://', '', $drupal_uri); // TODO: Maybe use _drush_sitealias_find_local_alias_name? $site_name = $drupal_root . '#' . $drupal_uri; } } return $site_name; } /** * If there are any path aliases (items beginning with "%") in the test * string, then resolve them as path aliases and add them to the provided * alias record. * * @param $alias_record * The full alias record to use in path alias expansion * @param $test_string * A slash-separated list of path aliases to resolve * e.g. "%files/%special". */ function drush_sitealias_resolve_path_references(&$alias_record, $test_string = '') { $path_aliases = array_key_exists('path-aliases', $alias_record) ? $alias_record['path-aliases'] : array(); // Convert the test string into an array of items, and // from this make a comma-separated list of projects // that we can pass to 'drush status'. $test_array = explode('/', $test_string); $project_array = array(); foreach($test_array as $one_item) { if (!empty($one_item) && ($one_item[0] == '%') && (!array_key_exists($one_item,$path_aliases))) { $project_array[] = substr($one_item,1); } } $project_list = implode(',', $project_array); if (!empty($project_array)) { // Optimization: if we're already bootstrapped to the // site specified by $alias_record, then we can just // call _core_site_status_table() rather than use backend invoke. if (drush_sitealias_is_bootstrapped_site($alias_record) && drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { $status_values = _core_site_status_table($project_list); } else { $values = drush_invoke_process($alias_record, "core-status", array(), empty($project_list) ? array() : array('project' => $project_list), array('integrate' => FALSE, 'override-simulated' => TRUE)); $status_values = $values['object']; } if (isset($status_values['%paths'])) { foreach ($status_values['%paths'] as $key => $path) { $alias_record['path-aliases'][$key] = $path; } } // If 'root' is not set in the alias, then fill it in from the status values. if (!isset($alias_record['root']) && isset($status_values['root'])) { $alias_record['root'] = $status_values['root']; } } } /** * Given an alias record that is a site list (contains a 'site-list' entry), * resolve all of the members of the site list and return them * is an array of alias records. * * @param $alias_record * The site list alias record array * @return * An array of individual site alias records */ function drush_sitealias_resolve_sitelist($alias_record) { $result_list = array(); if (isset($alias_record)) { if (array_key_exists('site-list', $alias_record)) { foreach ($alias_record['site-list'] as $sitespec) { $one_result = drush_sitealias_get_record($sitespec); $result_list = array_merge($result_list, drush_sitealias_resolve_sitelist($one_result)); } } elseif (array_key_exists('#name', $alias_record)) { $result_list[$alias_record['#name']] = $alias_record; } } return $result_list; } function _drush_sitelist_find_in_list($one_source, &$target) { $result = FALSE; foreach ($target as $key => $one_target) { if(_drush_sitelist_check_site_records($one_source, $one_target)) { $result = $one_target; unset($target[$key]); } } return $result; } function _drush_sitelist_check_site_records($source, $target) { if ((array_key_exists('uri', $source)) && (array_key_exists('uri', $target)) && ($source['uri'] == $target['uri'])) { return TRUE; } return FALSE; } /** * Initialize an alias record; called as soon as the alias * record is loaded from its alias file, before it is stored * in the cache. * * @param alias_record * The alias record to be initialized; parameter is modified in place. */ function _drush_sitealias_initialize_alias_record(&$alias_record) { // If there is a 'from-list' entry, then build a derived // list based on the site list with the given name. if (array_key_exists('from-list', $alias_record)) { // danger of infinite loops... move to transient defaults? $from_record = drush_sitealias_get_record($alias_record['from-list']); $from_list = drush_sitealias_resolve_sitelist($from_record); $derived_list = array(); foreach ($from_list as $one_record) { $derived_record = _drush_sitealias_derive_record($one_record, $alias_record); $derived_list[] = drush_sitealias_alias_record_to_spec($derived_record); } $alias_record = array(); if (!empty($derived_list)) { $alias_record['site-list'] = $derived_list; } } // If there is a 'site-search-path' entry, then build // a 'site-list' entry from all of the sites that can be // found in the search path. if (array_key_exists('site-search-path', $alias_record)) { // TODO: Is there any point in merging the sites from // the search path with any sites already listed in the // 'site-list' entry? For now we'll just overwrite. $search_path = $alias_record['site-search-path']; if (!is_array($search_path)) { $search_path = explode(',', $search_path); } $found_sites = _drush_sitealias_find_local_sites($search_path); $alias_record['site-list'] = $found_sites; // The 'unordered-list' flag indicates that the order of the items in the site list is not stable. $alias_record['unordered-list'] = '1'; // DEBUG: var_export($alias_record, FALSE); } if (array_key_exists('site-list', $alias_record)) { if (!is_array($alias_record['site-list'])) { $alias_record['site-list'] = explode(',', $alias_record['site-list']); } } else { if (isset($alias_record['root']) && !isset($alias_recort['uri'])) { $alias_recort['uri'] = 'default'; } } } /** * Add "static" default values to the given alias record. The * difference between a static default and a transient default is * that static defaults -always- exist in the alias record, and * they are cached, whereas transient defaults are only added * if the given drush command explicitly adds them. * * @param alias_record * An alias record with most values already filled in */ function _drush_sitealias_add_static_defaults(&$alias_record) { // If there is a 'db-url' entry but not 'databases' entry, then we will // build 'databases' from 'db-url' so that drush commands that use aliases // can always count on using a uniform 'databases' array. if (isset($alias_record['db-url']) && !isset($alias_record['databases'])) { $alias_record['databases'] = drush_sitealias_convert_db_from_db_url($alias_record['db-url']); } // Canonicalize paths. if (!empty($alias_record['root'])) { $alias_record['root'] = Path::canonicalize($alias_record['root']); } // Adjustments for aliases to drupal instances (as opposed to aliases that are site lists) if (array_key_exists('uri', $alias_record)) { // Make sure that there is always a 'path-aliases' array if (!array_key_exists('path-aliases', $alias_record)) { $alias_record['path-aliases'] = array(); } // If there is a 'root' entry, then copy it to the '%root' path alias if (isset($alias_record['root'])) { $alias_record['path-aliases']['%root'] = $alias_record['root']; } } } function _drush_sitealias_derive_record($from_record, $modifying_record) { $result = $from_record; // If there is a 'remote-user' in the modifying record, copy it. if (array_key_exists('remote-user', $modifying_record)) { $result['remote-user'] = $from_record['remote_user']; } // If there is a 'remote-host', then: // If it is empty, clear the remote host in the result record // If it ends in '.', then prepend it to the remote host in the result record // Otherwise, copy it to the result record if (array_key_exists('remote-host', $modifying_record)) { $remote_host_modifier = $modifying_record['remote-host']; if(empty($remote_host_modifier)) { unset($result['remote-host']); unset($result['remote-user']); } elseif ($remote_host_modifier[strlen($remote_host_modifier)-1] == '.') { $result['remote-host'] = $remote_host_modifier . $result['remote-host']; } else { $result['remote-host'] = $remote_host_modifier; } } // If there is a 'root', then: // If it begins with '/', copy it to the result record // Otherwise, append it to the result record if (array_key_exists('root', $modifying_record)) { $root_modifier = $modifying_record['root']; if($root_modifier[0] == '/') { $result['root'] = $root_modifier; } else { $result['root'] = $result['root'] . '/' . $root_modifier; } } // Poor man's realpath: take out the /../ with preg_replace. // (realpath fails if the files in the path do not exist) while(strpos($result['root'], '/../') !== FALSE) { $result['root'] = preg_replace('/\w+\/\.\.\//', '', $result['root']); } // TODO: Should we allow the uri to be transformed? // I think that if the uri does not match, then you should // always build the list by hand, and not rely on '_drush_sitealias_derive_record'. return $result; } /** * Convert from an alias record to a site specification * * @param alias_record * The full alias record to convert * * @param with_db * True if the site specification should include a ?db-url term * * @return string * The site specification */ function drush_sitealias_alias_record_to_spec($alias_record, $with_db = false) { $result = ''; // TODO: we should handle 'site-list' records too. if (array_key_exists('site-list', $alias_record)) { // TODO: we should actually expand the site list and recompose it $result = implode(',', $alias_record['site-list']); } else { // There should always be a uri if (array_key_exists('uri', $alias_record)) { $result = '#' . drush_sitealias_uri_to_site_dir($alias_record['uri'], drush_sitealias_get_root($alias_record)); } // There should always be a root if (array_key_exists('root', $alias_record)) { $result = $alias_record['root'] . $result; } if (array_key_exists('remote-host', $alias_record)) { $result = drush_remote_host($alias_record) . $result; } // Add the database info to the specification if desired if ($with_db) { // If db-url is not supplied, look it up from the remote // or local site and add it to the site alias if (!isset($alias_record['db-url'])) { drush_sitealias_add_db_url($alias_record); } $result = $result . '?db-url=' . urlencode(is_array($alias_record['db-url']) ? $alias_record['db-url']['default'] : $alias_record['db-url']); } } return $result; } /** * Search for drupal installations in the search path. * * @param search_path * An array of drupal root folders * * @return * An array of site specifications (/path/to/root#sitename.com) */ function _drush_sitealias_find_local_sites($search_path) { $result = array(); foreach ($search_path as $a_drupal_root) { $result = array_merge($result, _drush_find_local_sites_at_root($a_drupal_root)); } return $result; } /** * Return a list of all of the local sites at the specified drupal root. */ function _drush_find_local_sites_at_root($a_drupal_root = '', $search_depth = 1) { $site_list = array(); $base_path = (empty($a_drupal_root) ? drush_get_context('DRUSH_DRUPAL_ROOT') : $a_drupal_root ); if (!empty($base_path)) { if (drush_valid_root($base_path)) { // If $a_drupal_root is in fact a valid drupal root, then return // all of the sites found inside the 'sites' folder of this drupal instance. $site_list = _drush_find_local_sites_in_sites_folder($base_path); } else { $bootstrap_files = drush_scan_directory($base_path, '/' . basename(DRUSH_DRUPAL_SIGNATURE) . '/' , array('.', '..', 'CVS', 'examples'), 0, drush_get_option('search-depth', $search_depth) + 1, 'filename', 1); foreach ($bootstrap_files as $one_bootstrap => $info) { $includes_dir = dirname($one_bootstrap); if (basename($includes_dir) == basename(dirname(DRUSH_DRUPAL_SIGNATURE))) { $drupal_root = dirname($includes_dir); $site_list = array_merge(_drush_find_local_sites_in_sites_folder($drupal_root), $site_list); } } } } return $site_list; } /** * Return a list of all of the local sites at the specified 'sites' folder. */ function _drush_find_local_sites_in_sites_folder($a_drupal_root) { $site_list = array(); // If anyone searches for sites at a given root, then // make sure that alias files stored at this root // directory are included in the alias search path drush_sitealias_add_to_alias_path($a_drupal_root); $base_path = $a_drupal_root . '/sites'; // TODO: build a cache keyed off of $base_path (realpath($base_path)?), // so that it is guarenteed that the lists returned will definitely be // exactly the same should this routine be called twice with the same path. $files = drush_scan_directory($base_path, '/settings\.php/', array('.', '..', 'CVS', 'all'), 0, 1, 'filename', 1); foreach ($files as $filename => $info) { if ($info->basename == 'settings.php') { // First we'll resolve the realpath of the settings.php file, // so that we get the correct drupal root when symlinks are in use. $real_sitedir = dirname(realpath($filename)); $real_root = drush_locate_root($filename); if ($real_root !== FALSE) { $a_drupal_site = $real_root . '#' . basename($real_sitedir); } // If the symlink points to some folder outside of any drupal // root, then we'll use the else { $uri = drush_sitealias_site_dir_from_filename($filename); $a_drupal_site = $a_drupal_root . '#' . $uri; } // Add the site if it isn't already in the array if (!in_array($a_drupal_site, $site_list)) { $site_list[] = $a_drupal_site; } } } return $site_list; } function drush_sitealias_create_sites_alias($a_drupal_root = '') { $sites_list = _drush_find_local_sites_at_root($a_drupal_root); _drush_sitealias_cache_alias('@sites', array('site-list' => $sites_list)); } /** * Add "transient" default values to the given alias record. The * difference between a static default and a transient default is * that static defaults -always- exist in the alias record, * whereas transient defaults are only added if the given drush * command explicitly calls this function. The other advantage * of transient defaults is that it is possible to differentiate * between a default value and an unspecified value, since the * transient defaults are not added until requested. * * Since transient defaults are not cached, you should avoid doing * expensive operations here. To be safe, drush commands should * avoid calling this function more than once. * * @param alias_record * An alias record with most values already filled in */ function _drush_sitealias_add_transient_defaults(&$alias_record) { if (isset($alias_record['path-aliases'])) { // Add the path to the drush folder to the path aliases as !drush if (!array_key_exists('%drush', $alias_record['path-aliases'])) { if (array_key_exists('%drush-script', $alias_record['path-aliases'])) { $alias_record['path-aliases']['%drush'] = dirname($alias_record['path-aliases']['%drush-script']); } else { $alias_record['path-aliases']['%drush'] = dirname(drush_find_drush()); } } // Add the path to the site folder to the path aliases as !site if (!array_key_exists('%site', $alias_record['path-aliases']) && array_key_exists('uri', $alias_record)) { $alias_record['path-aliases']['%site'] = 'sites/' . drush_sitealias_uri_to_site_dir($alias_record['uri'], drush_sitealias_get_root($alias_record)) . '/'; } } } /** * Find the name of a local alias record that has the specified * root and uri. */ function _drush_sitealias_find_local_alias_name($root, $uri) { $result = ''; $all_site_aliases =& drush_get_context('site-aliases'); foreach ($all_site_aliases as $alias_name => $alias_values) { if (!array_key_exists('remote-host', $alias_values) && array_key_exists('root', $alias_values) && array_key_exists('uri', $alias_values) && ($alias_name != '@self')) { if (($root == $alias_values['root']) && ($uri == $alias_values['uri'])) { $result = $alias_name; } } } return $result; } /** * If '$alias' is the name of a folder in the sites folder of the given drupal * root, then build an alias record for it * * @param alias * The name of the site in the 'sites' folder to convert * @return array * An alias record, or empty if none found. */ function _drush_sitealias_find_record_for_local_site($alias, $drupal_root = NULL) { $alias_record = array(); // Clip off the leading '#' if it is there if (substr($alias,0,1) == '#') { $alias = substr($alias,1); } if (!isset($drupal_root)) { $drupal_root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); } if (!empty($drupal_root)) { $alias_dir = drush_sitealias_uri_to_site_dir($alias, $drupal_root); $site_settings_file = $drupal_root . '/sites/' . $alias_dir . '/settings.php'; $alias_record = drush_sitealias_build_record_from_settings_file($site_settings_file, $alias, $drupal_root); } return $alias_record; } function drush_sitealias_build_record_from_settings_file($site_settings_file, $alias = null, $drupal_root = null) { $alias_record = array(); if (file_exists($site_settings_file)) { if (!isset($drupal_root)) { $drupal_root = drush_locate_root($site_settings_file); } $alias_record['root'] = $drupal_root; if (isset($alias)) { $alias_record['uri'] = $alias; } else { $alias_record['uri'] = _drush_sitealias_site_dir_to_uri(drush_sitealias_site_dir_from_filename($site_settings_file)); } } return $alias_record; } /** * Pull the site directory from the path to settings.php * * @param site_settings_file * path to settings.php * * @return string * the site directory component of the path to settings.php */ function drush_sitealias_site_dir_from_filename($site_settings_file) { return basename(dirname($site_settings_file)); } /** * Convert from a URI to a site directory. * * @param uri * A uri, such as http://domain.com:8080/drupal * @return string * A directory, such as domain.com.8080.drupal */ function drush_sitealias_uri_to_site_dir($uri, $site_root = NULL) { $uri = str_replace('http://', '', $uri); $uri = str_replace('https://', '', $uri); if (drush_is_windows()) { // Handle absolute paths on windows $uri = str_replace(array(':/', ':\\'), array('.', '.'), $uri); } $hostname = str_replace(array('/', ':', '\\'), array('.', '.', '.'), $uri); // Check sites.php mappings $site_dir = drush_site_dir_lookup_from_hostname($hostname, $site_root); return $site_dir ? $site_dir : $hostname; } /** * Convert from an old-style database URL to an array of database settings. * * @param db_url * A Drupal 6 db url string to convert, or an array with a 'default' element. * @return array * An array of database values containing only the 'default' element of * the db url. If the parse fails the array is empty. */ function drush_convert_db_from_db_url($db_url) { $db_spec = array(); if (is_array($db_url)) { $db_url_default = $db_url['default']; } else { $db_url_default = $db_url; } // If it's a sqlite database, pick the database path and we're done. if (strpos($db_url_default, 'sqlite://') === 0) { $db_spec = array( 'driver' => 'sqlite', 'database' => substr($db_url_default, strlen('sqlite://')), ); } else { $url = parse_url($db_url_default); if ($url) { // Fill in defaults to prevent notices. $url += array( 'scheme' => NULL, 'user' => NULL, 'pass' => NULL, 'host' => NULL, 'port' => NULL, 'path' => NULL, ); $url = (object)array_map('urldecode', $url); $db_spec = array( 'driver' => $url->scheme == 'mysqli' ? 'mysql' : $url->scheme, 'username' => $url->user, 'password' => $url->pass, 'host' => $url->host, 'port' => $url->port, 'database' => ltrim($url->path, '/'), ); } } return $db_spec; } /** * Convert from an old-style database URL to an array of database settings * * @param db_url * A Drupal 6 db-url string to convert, or an array with multiple db-urls. * @return array * An array of database values. */ function drush_sitealias_convert_db_from_db_url($db_url) { $result = array(); if (!is_array($db_url)) { $result = array('default' => array('default' => drush_convert_db_from_db_url($db_url))); } else { foreach ($db_url as $one_name => $one_db_url) { $result[$one_name] = array('default' => drush_convert_db_from_db_url($one_db_url)); } } return $result; } /** * Utility function used by drush_get_alias; keys that start with * '%' or '!' are path aliases, the rest are entries in the alias record. */ function _drush_sitealias_set_record_element(&$alias_record, $key, $value) { if ((substr($key,0,1) == '%') || (substr($key,0,1) == '!')) { $alias_record['path-aliases'][$key] = $value; } elseif (!empty($key)) { $alias_record[$key] = $value; } } /** * Looks up the specified alias record and calls through to * drush_sitealias_set_alias_context, below. * * @param alias * The name of the alias record * @param prefix * The prefix value to afix to the beginning of every * key set. * @return boolean * TRUE is an alias was found and processed. */ function _drush_sitealias_set_context_by_name($alias, $prefix = '') { if ($alias) { $site_alias_settings = drush_sitealias_get_record($alias); if (!empty($site_alias_settings)) { drush_sitealias_set_alias_context($site_alias_settings, $prefix); drush_sitealias_cache_alias_by_path($site_alias_settings); if (empty($prefix)) { // Create an alias '@self' // Allow 'uri' from the commandline to override $drush_uri = drush_get_option(array('uri', 'l'), FALSE); if ($drush_uri) { $site_alias_settings['uri'] = $drush_uri; } _drush_sitealias_cache_alias('@self', $site_alias_settings); // Change the selected site to match the new --root and --uri, if any were set. _drush_preflight_root_uri(); } return $site_alias_settings; } } return array(); } /** * Given an alias record, overwrite its values with options * from the command line and other drush contexts as specified * by the provided prefix. For example, if the prefix is 'source-', * then any option 'source-foo' will set the value 'foo' in the * alias record. */ function drush_sitealias_overlay_options($site_alias_record, $prefix) { return array_merge($site_alias_record, drush_get_merged_prefixed_options($prefix)); } /** * First return an option set via drush_sitealias_overlay_options, if * any, then fall back on "%" . $option from the path aliases. */ function drush_sitealias_get_path_option($site_alias_record, $option, $default = NULL) { if (isset($site_alias_record) && array_key_exists($option, $site_alias_record)) { return $site_alias_record[$option]; } if (isset($site_alias_record) && array_key_exists('path-aliases', $site_alias_record) && array_key_exists("%$option", $site_alias_record['path-aliases'])) { return $site_alias_record['path-aliases']["%$option"]; } else { return drush_get_option($option, $default); } } /** * Given a site alias record, copy selected fields from it * into the drush 'alias' context. The 'alias' context has * lower precedence than the 'cli' context, so values * set by an alias record can be overridden by command-line * parameters. * * @param site_alias_settings * An alias record * @param prefix * The prefix value to affix to the beginning of every * key set. For example, if this function is called once with * 'source-' and again with 'destination-' prefixes, then the * source database records will be stored in 'source-databases', * and the destination database records will be in * 'destination-databases'. */ function drush_sitealias_set_alias_context($site_alias_settings, $prefix = '') { $options = drush_get_context('alias'); // There are some items that we should just skip $skip_list = drush_get_special_keys(); // If 'php-options' are set in the alias, then we will force drush // to redispatch via the remote dispatch mechanism even if the target is localhost. if ((array_key_exists('php-options', $site_alias_settings) || array_key_exists('php', $site_alias_settings)) && !drush_get_context('DRUSH_BACKEND', FALSE)) { if (!array_key_exists('remote-host', $site_alias_settings)) { $site_alias_settings['remote-host'] = 'localhost'; } } // If 'php-options' are not set in the alias, then skip 'remote-host' // and 'remote-user' if 'remote-host' is actually the local machine. // This prevents drush from using the remote dispatch mechanism (the command // is just run directly on the local machine, bootstrapping to the specified alias) elseif (array_key_exists('remote-host', $site_alias_settings) && drush_is_local_host($site_alias_settings['remote-host'])) { $skip_list[] = 'remote-host'; $skip_list[] = 'remote-user'; } // If prefix is set, then copy from the 'prefix-' version // of the drush special keys ('command-specific', 'path-aliases') // into the ordinary version. This will allow us to set // 'source-command-specific' options that will only apply when // the alias is used as the source option for rsync or sql-sync. if (!empty($prefix)) { $special_contexts = drush_get_special_keys(); foreach ($special_contexts as $option_name) { if (array_key_exists($prefix . $option_name, $site_alias_settings)) { $site_alias_settings[$option_name] = array_key_exists($option_name, $site_alias_settings) ? array_merge($site_alias_settings[$option_name], $site_alias_settings[$prefix . $option_name]) : $site_alias_settings[$prefix . $option_name]; } } } // Transfer all options from the site alias to the drush options // in the 'alias' context. foreach ($site_alias_settings as $key => $value) { // Special handling for path aliases: if ($key == "path-aliases") { $path_aliases = $value; foreach (array('%drush-script', '%dump', '%dump-dir', '%include') as $path_key) { if (array_key_exists($path_key, $path_aliases)) { // Evaluate the path value, and substitute any path references found. // ex: '%dump-dir' => '%root/dumps' will store sql-dumps in the folder // 'dumps' in the Drupal root folder for the site. $evaluated_path = str_replace(array_keys($path_aliases), array_values($path_aliases), $path_aliases[$path_key]); $options[$prefix . substr($path_key, 1)] = $evaluated_path; } } } // Special handling for command-specific elseif ($key == "command-specific") { $options[$key] = $value; } elseif (!in_array($key, $skip_list)) { $options[$prefix . $key] = $value; } } drush_set_config_options('alias', $options); } /** * Call prior to drush_sitealias_evaluate_path to insure * that any site-specific aliases associated with any * local site in $path are defined. */ function _drush_sitealias_preflight_path($path) { $alias = NULL; // Parse site aliases if there is a colon in the path // We allow: // @alias:/path // machine.domain.com:/path // machine:/path // Note that paths in the form "c:/path" are converted to // "/cygdrive/c/path" later; we do not want them to confuse // us here, so we skip paths that start with a single character // before the colon if we are running on Windows. Single-character // machine names are allowed in Linux only. $colon_pos = strpos($path, ':'); if ($colon_pos > (drush_is_windows("LOCAL") ? 1 : 0)) { $alias = substr($path, 0, $colon_pos); $path = substr($path, $colon_pos + 1); $site_alias_settings = drush_sitealias_get_record($alias); if (empty($site_alias_settings) && (substr($path,0,1) == '@')) { return NULL; } $machine = $alias; } else { $machine = ''; // if the path is a site alias or a local site... $site_alias_settings = drush_sitealias_get_record($path); if (empty($site_alias_settings) && (substr($path,0,1) == '@')) { return NULL; } if (!empty($site_alias_settings) || drush_is_local_host($path)) { $alias = $path; $path = ''; } } return array('alias' => $alias, 'path' => $path, 'machine' => $machine); } /** * Given a properly-escaped options string, replace any occurance of * %files and so on embedded inside it with its corresponding path. */ function drush_sitealias_evaluate_paths_in_options($option_string) { $path_aliases = _core_path_aliases(); return str_replace(array_keys($path_aliases), array_values($path_aliases), $option_string); } /** * Evaluate a path from its shorthand form to a literal path * usable by rsync. * * A path is "machine:/path" or "machine:path" or "/path" or "path". * 'machine' might instead be an alias record, or the name * of a site in the 'sites' folder. 'path' might be (or contain) * '%root' or some other path alias. This function will examine * all components of the path and evaluate them as necessary to * come to the final path. * * @param path * The path to evaluate * @param additional_options * An array of options that overrides whatever was passed in on * the command line (like the 'process' context, but only for * the scope of this one call). * @param local_only * If TRUE, force an error if the provided path points to a remote * machine. * @param os * This should be the local system os, unless evaluate path is * being called for rsync, in which case it should be "CWRSYNC" * if cwrsync is being used, or "rsync" to automatically select * between "LOCAL" and "CWRSYNC" based on the platform. * @return * The site record for the machine specified in the path, if any, * with the path to pass to rsync (including the machine specifier) * in the 'evaluated-path' item. */ function drush_sitealias_evaluate_path($path, &$additional_options, $local_only = FALSE, $os = NULL, $command_specific_prefix = '') { $site_alias_settings = array(); $path_aliases = array(); $remote_user = ''; $preflight = _drush_sitealias_preflight_path($path); if (!isset($preflight)) { return NULL; } $alias = $preflight['alias']; $path = $preflight['path']; $machine = $preflight['machine']; if (isset($alias)) { // Note that the alias settings may have an 'os' component, but we do // not want to use it here. The paths passed to rsync should always be // escaped per the LOCAL rules, without regard to the remote platform type. $site_alias_settings = drush_sitealias_get_record($alias); if (!empty($command_specific_prefix)) { drush_command_set_command_specific_options($command_specific_prefix); drush_sitealias_command_default_options($site_alias_settings, $command_specific_prefix); } } if (!empty($site_alias_settings)) { if ($local_only && array_key_exists('remote-host', $site_alias_settings)) { return drush_set_error('DRUSH_REMOTE_SITE_IN_LOCAL_CONTEXT', dt("A remote site alias was used in a context where only a local alias is appropriate.")); } // Apply any options from this alias that might affect our rsync drush_sitealias_set_alias_context($site_alias_settings); // Use 'remote-host' from settings if available; otherwise site is local if (drush_sitealias_is_remote_site($site_alias_settings)) { $machine = drush_remote_host($site_alias_settings); } else { $machine = ''; } } else { // Strip the machine portion of the path if the // alias points to the local machine. if (drush_is_local_host($machine)) { $machine = ''; } else { $machine = "$remote_user$machine"; } } // TOD: The code below is a little rube-goldberg-ish, and needs to be // reworked. core-rsync will call this function twice: once to // evaluate the destination, and then again to evaluate the source. Things // get odd with --exclude-paths, especially in conjunction with command-specific // and the --exclude-files option. @see testCommandSpecific() // If the --exclude-other-sites option is specified, then // convert that into --include-paths='%site' and --exclude-sites. if (drush_get_option_override($additional_options, 'exclude-other-sites', FALSE) && !drush_get_context('exclude-other-sites-processed', FALSE)) { $include_path_option = drush_get_option_override($additional_options, 'include-paths', ''); $additional_options['include-paths'] = '%site'; if (!empty($include_path_option)) { // We use PATH_SEPARATOR here because we are later going to explicitly explode() this variable using PATH_SEPARATOR. $additional_options['include-paths'] .= PATH_SEPARATOR . $include_path_option; } $additional_options['exclude-sites'] = TRUE; drush_set_context('exclude-other-sites-processed', TRUE); } else { unset($additional_options['include-paths']); } // If the --exclude-files option is specified, then // convert that into --exclude-paths='%files'. if (drush_get_option_override($additional_options, 'exclude-files', FALSE) && !drush_get_option_override($additional_options, 'exclude-files-processed', FALSE, 'process')) { $exclude_path_option = drush_get_option_override($additional_options, 'exclude-paths', ''); $additional_options['exclude-paths'] = '%files'; if (!empty($exclude_path_option)) { // We use PATH_SEPARATOR here because we are later going to explicitly explode() this variable using PATH_SEPARATOR. $additional_options['exclude-paths'] .= PATH_SEPARATOR . $exclude_path_option; } $additional_options['exclude-files-processed'] = TRUE; } else { unset($additional_options['exclude-paths']); } // If there was no site specification given, and the // machine is local, then try to look // up an alias record for the default drush site. if (empty($site_alias_settings) && empty($machine)) { $drush_uri = drush_get_context('DRUSH_SELECTED_URI', 'default'); $site_alias_settings = drush_sitealias_get_record($drush_uri); } // Always add transient defaults _drush_sitealias_add_transient_defaults($site_alias_settings); // The $resolve_path variable is used by drush_sitealias_resolve_path_references // to test to see if there are any path references such as %site or %files // in it, so that resolution is only done if the path alias is referenced. // Therefore, we can concatenate without worrying too much about the structure of // this variable's contents. $include_path = drush_get_option_override($additional_options, 'include-paths', ''); $exclude_path = drush_get_option_override($additional_options, 'exclude-paths', ''); if (is_array($include_path)) { $include_path = implode('/', $include_path); } if (is_array($exclude_path)) { $include_path = implode('/', $exclude_path); } $resolve_path = "$path/$include_path/$exclude_path"; // Resolve path aliases such as %files, if any exist in the path if (!empty($resolve_path)) { drush_sitealias_resolve_path_references($site_alias_settings, $resolve_path); } if (array_key_exists('path-aliases', $site_alias_settings)) { $path_aliases = $site_alias_settings['path-aliases']; } // Get the 'root' setting from the alias; if it does not // exist, then get the root from the bootstrapped site. if (array_key_exists('root', $site_alias_settings)) { $drupal_root = $site_alias_settings['root']; } elseif (!drush_sitealias_is_remote_site($site_alias_settings)) { drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_SITE); $drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT'); } if (empty($drupal_root)) { $drupal_root = ''; } else { // Add a slash to the end of the drupal root, as below. $drupal_root = drush_trim_path($drupal_root) . "/"; } $full_path_aliases = $path_aliases; foreach ($full_path_aliases as $key => $value) { // Expand all relative path aliases to be based off of the Drupal root if (!drush_is_absolute_path($value, "LOCAL") && ($key != '%root')) { $full_path_aliases[$key] = $drupal_root . $value; } // We do not want slashes on the end of our path aliases. $full_path_aliases[$key] = drush_trim_path($full_path_aliases[$key]); } // Fill in path aliases in the path, the include path and the exclude path. $path = str_replace(array_keys($full_path_aliases), array_values($full_path_aliases), $path); if (!empty($include_path)) { drush_set_option('include-paths', str_replace(array_keys($path_aliases), array_values($path_aliases), $include_path)); } if (!empty($exclude_path)) { drush_set_option('exclude-paths', str_replace(array_keys($path_aliases), array_values($path_aliases), $exclude_path)); } // Next make the rsync path, which includes the machine // and path components together. // First make empty paths or relative paths start from the drupal root. if (empty($path) || (!drush_is_absolute_path($path, "LOCAL"))) { $path = $drupal_root . $path; } // When calculating a path for use with rsync, we must correct // absolute paths in the form c:\path when cwrsync is in use. $path = drush_correct_absolute_path_for_exec($path, $os); // If there is a $machine component, to the path, then // add it to the beginning $evaluated_path = drush_escapeshellarg($path, $os); if (!empty($machine)) { $evaluated_path = $machine . ':' . $evaluated_path; } // // Add our result paths: // // evaluated-path: machine:/path // server-component: machine // path-component: :/path // path: /path // user-path: path (as specified in input parameter) // $site_alias_settings['evaluated-path'] = $evaluated_path; if (!empty($machine)) { $site_alias_settings['server-component'] = $machine; } $site_alias_settings['path-component'] = (!empty($path) ? ':' . $path : ''); $site_alias_settings['path'] = $path; $site_alias_settings['user-path'] = $preflight['path']; return $site_alias_settings; } /** * Option keys used for site selection. */ function drush_sitealias_site_selection_keys() { return array('remote-host', 'remote-user', 'ssh-options', '#name', 'os'); } function sitealias_find_local_drupal_root($site_list) { $drupal_root = NULL; foreach ($site_list as $site) { if (($drupal_root == NULL) && (array_key_exists('root', $site) && !array_key_exists('remote-host', $site))) { $drupal_root = $site['root']; } } return $drupal_root; } /** * Helper function to obtain the keys' names that need special handling in certain * cases. * @return * A non-associative array containing the needed keys' names. */ function drush_get_special_keys() { $special_keys = array( 'command-specific', 'site-aliases', ); return $special_keys; } /** * Read the tmp file where the persistent site setting is stored. * * @return string * A valid site specification. */ function drush_sitealias_site_get() { if (($filename = drush_sitealias_get_envar_filename()) && file_exists($filename)) { $site = file_get_contents($filename); return $site; } else { return FALSE; } } /** * Un-set the currently use'd site alias. */ function drush_sitealias_site_clear() { if ($filename = drush_sitealias_get_envar_filename()) { return drush_delete_dir($filename); } return FALSE; } /** * Returns the filename for the file that stores the DRUPAL_SITE variable. * * @param string $filename_prefix * An arbitrary string to prefix the filename with. * * @return string|false * Returns the full path to temp file if possible, or FALSE if not. */ function drush_sitealias_get_envar_filename($filename_prefix = 'drush-drupal-site-') { $shell_pid = getenv('DRUSH_SHELL_PID'); if (!$shell_pid && function_exists('posix_getppid')) { $shell_pid = posix_getppid(); } if (!$shell_pid) { return FALSE; } $tmp = getenv('TMPDIR') ? getenv('TMPDIR') : '/tmp'; $username = drush_get_username(); return "{$tmp}/drush-env-{$username}/{$filename_prefix}" . $shell_pid; } /** * Cache the specified alias in the alias path cache. The * alias path cache creates a lookup from the site folder * (/path/to/drupal/sites/default) to the provided alias record. * * Only the name of the alias and the path to the file it * is stored in is cached; when it is retrieved, it is * loaded directly from the correct file. */ function drush_sitealias_cache_alias_by_path($alias_record) { if (!isset($alias_record['remote-host']) && isset($alias_record['root']) && isset($alias_record['uri']) && isset($alias_record['#name']) && isset($alias_record['#file'])) { $path = drush_sitealias_local_site_path($alias_record); if ($path) { $cid = drush_get_cid('alias-path-', array(), array($path)); $alias_path_data = array( '#name' => $alias_record['#name'], '#file' => $alias_record['#file'], ); drush_cache_set($cid, $alias_path_data); } } } /** * Look for a defined alias that points to the specified * site directory. The cache is tested first; if nothing * is cached, then an exhaustive search is done for the * specified site. If the exhaustive search returns a * match, then it is cached. * * @param $path * /path/to/drupal/sites/default * @return * An alias record for the provided path */ function drush_sitealias_lookup_alias_by_path($path, $allow_best_match=FALSE) { $result = drush_sitealias_quick_lookup_cached_alias_by_path($path); $fallback = array(); if (empty($result)) { $aliases = _drush_sitealias_find_and_load_all_aliases(); foreach ($aliases as $name => $alias_record) { if (!isset($alias_record['remote-host']) && isset($alias_record['root']) && isset($alias_record['uri']) && isset($alias_record['#name']) && isset($alias_record['#file'])) { if ($path == drush_sitealias_local_site_path($alias_record)) { $result = $alias_record; break; } if (substr($path, 0, strlen($alias_record['root'])) == $alias_record['root']) { $fallback = $alias_record; } } } } if (empty($result) && $allow_best_match) { $result = $fallback; } if (!empty($result)) { _drush_sitealias_add_inherited_values_to_record($result); drush_sitealias_cache_alias_by_path($result); } return $result; } /** * Look for a cached alias that points to the specified * site directory. Nothing is returned if there is no * matching cached alias. * * @param $path * /path/to/drupal/sites/default * @return * An alias record for the provided path */ function drush_sitealias_quick_lookup_cached_alias_by_path($path) { $alias_record = array(); $cid = drush_get_cid('alias-path-', array(), array($path)); $alias_path_cache = drush_cache_get($cid); if (isset($alias_path_cache->data)) { $alias_name = $alias_path_cache->data['#name']; $alias_file = $alias_path_cache->data['#file']; $alias_record = _drush_sitealias_find_and_load_alias_from_file($alias_name, array($alias_file)); _drush_sitealias_add_inherited_values_to_record($alias_record); $alias_record['#name'] = $alias_name; } return $alias_record; } /** * Return the site root, if there is one in the record. */ function drush_sitealias_get_root($alias_record) { return array_key_exists('root', $alias_record) ? $alias_record['root'] : NULL; } /** * Decide on which side to run a core-rsync. * * @param $source * @param $destination * @param $runner Where to run the rsync operation: 'destination', 'source', * 'auto' ('destination' if both are remote, otherwise '@self') or FALSE (@self) * @return mixed */ function drush_get_runner($source, $destination, $runner = FALSE) { if (is_string($source)) { $source = drush_sitealias_get_record($site); } if (is_string($destination)) { $destination = drush_sitealias_get_record($destination); } // If both sites are remote, and --runner=auto, then we'll use the destination site. if (drush_sitealias_is_remote_site($source) && drush_sitealias_is_remote_site($destination)) { if ($runner == 'auto') { $runner = 'destination'; } } // If the user explicitly requests a remote site, then return the selected one. if ($runner == 'destination') { return "@" . $destination['#name']; } if ($runner == 'source') { return "@" . $source['#name']; } // Default to running rsync locally. When in doubt, local is best, because // we can always resolve aliases here. return '@self'; } 'key']. We add a default // value of [1 => 'value'] to cover this case. If // explode returns two items, the default value is ignored. list($key, $value) = explode('=', $item, 2) + array(1 => ''); $env[$key] = $value; } } return $env; } /** * Checks the provided location and return the appropriate * Drush wrapper or Drush launcher script, if found. * * If the provided location looks like it might be a web * root (i.e., it contains an index.php), then we will search * in a number of locations in the general vicinity of the * web root for a Drush executable. * * For other locations, we will look only in that specific * directory, or in vendor/bin. */ function find_wrapper_or_launcher($location) { if (file_exists($location. DIRECTORY_SEPARATOR. 'index.php')) { return find_wrapper_or_launcher_in_vicinity($location); } return find_wrapper_or_launcher_in_specific_locations($location, ["", 'vendor'. DIRECTORY_SEPARATOR. 'bin']); } /** * We look for a "Drush wrapper" script that might * be stored in the root of a site. If there is * no wrapper script, then we look for the * drush.launcher script in vendor/bin. We try just a * few of the most common locations; if the user relocates * their vendor directory anywhere else, then they must * use a wrapper script to locate it. See the comment in * 'examples/drush' for details. */ function find_wrapper_or_launcher_in_vicinity($location) { $sep = DIRECTORY_SEPARATOR; $drush_locations = [ "", "vendor{$sep}bin/", "..{$sep}vendor{$sep}bin{$sep}", "sites{$sep}all{$sep}vendor{$sep}bin{$sep}", "sites{$sep}all{$sep}vendor{$sep}drush{$sep}drush{$sep}", "sites{$sep}all{$sep}drush{$sep}drush{$sep}", "drush{$sep}drush{$sep}", ]; return find_wrapper_or_launcher_in_specific_locations($location, $drush_locations); } function find_wrapper_or_launcher_in_specific_locations($location, $drush_locations) { $sep = DIRECTORY_SEPARATOR; foreach ($drush_locations as $d) { $found_script = find_wrapper_or_launcher_at_location("$location$sep$d"); if (!empty($found_script)) { return $found_script; } } return ""; } /** * We are somewhat "loose" about whether we are looking * for "drush" or "drush.launcher", because in old versions * of Drush, the "drush launcher" was named "drush". * Otherwise, there wouldn't be any point in looking for * "drush.launcher" at the root, or "drush" in a vendor directory. * We also allow users to rename their drush wrapper to * 'drush.wrapper' to avoid conflicting with a directory named * 'drush' at the site root. */ function find_wrapper_or_launcher_at_location($location) { $sep = DIRECTORY_SEPARATOR; // Sanity-check: empty $location means that we should search // at the cwd, not at the root of the filesystem. if (empty($location)) { $location = "."; } foreach (array('.launcher', '.wrapper', '') as $suffix) { $check_location = "$location{$sep}drush$suffix"; if (is_file($check_location)) { return $check_location; } } return ""; } /** * Determine whether current OS is a Windows variant. */ function drush_is_windows($os = NULL) { return strtoupper(substr($os ?: PHP_OS, 0, 3)) === 'WIN'; } function drush_escapeshellarg($arg, $os = NULL, $raw = FALSE) { // Short-circuit escaping for simple params (keep stuff readable) if (preg_match('|^[a-zA-Z0-9.:/_-]*$|', $arg)) { return $arg; } elseif (drush_is_windows($os)) { return _drush_escapeshellarg_windows($arg, $raw); } else { return _drush_escapeshellarg_linux($arg, $raw); } } /** * Linux version of escapeshellarg(). * * This is intended to work the same way that escapeshellarg() does on * Linux. If we need to escape a string that will be used remotely on * a Linux system, then we need our own implementation of escapeshellarg, * because the Windows version behaves differently. */ function _drush_escapeshellarg_linux($arg, $raw = FALSE) { // For single quotes existing in the string, we will "exit" // single-quote mode, add a \' and then "re-enter" // single-quote mode. The result of this is that // 'quote' becomes '\''quote'\'' $arg = preg_replace('/\'/', '\'\\\'\'', $arg); // Replace "\t", "\n", "\r", "\0", "\x0B" with a whitespace. // Note that this replacement makes Drush's escapeshellarg work differently // than the built-in escapeshellarg in PHP on Linux, as these characters // usually are NOT replaced. However, this was done deliberately to be more // conservative when running _drush_escapeshellarg_linux on Windows // (this can happen when generating a command to run on a remote Linux server.) $arg = str_replace(array("\t", "\n", "\r", "\0", "\x0B"), ' ', $arg); // Only wrap with quotes when needed. if(!$raw) { // Add surrounding quotes. $arg = "'" . $arg . "'"; } return $arg; } /** * Windows version of escapeshellarg(). */ function _drush_escapeshellarg_windows($arg, $raw = FALSE) { // Double up existing backslashes $arg = preg_replace('/\\\/', '\\\\\\\\', $arg); // Double up double quotes $arg = preg_replace('/"/', '""', $arg); // Double up percents. // $arg = preg_replace('/%/', '%%', $arg); // Only wrap with quotes when needed. if(!$raw) { // Add surrounding quotes. $arg = '"' . $arg . '"'; } return $arg; } /** * drush_startup is called once, by the Drush "finder" * script -- the "drush" script at the Drush root. * It finds the correct Drush "wrapper" or "launcher" * script to use, and executes it with process replacement. */ function drush_startup($argv) { $sep = DIRECTORY_SEPARATOR; $found_script = ""; $cwd = getcwd(); $home = getenv("HOME"); $use_dir = "$home{$sep}.drush{$sep}use"; // Get the arguments for the command. Shift off argv[0], // which contains the name of this script. $arguments = $argv; array_shift($arguments); // We need to do at least a partial parsing of the options, // so that we can find --root / -r and so on. $VERBOSE=FALSE; $DEBUG=FALSE; $ROOT=FALSE; $COMMAND=FALSE; $ALIAS=FALSE; $VAR=FALSE; foreach ($arguments as $arg) { // If a variable to set was indicated on the // previous iteration, then set the value of // the named variable (e.g. "ROOT") to "$arg". if ($VAR) { $$VAR = "$arg"; $VAR = FALSE; } else { switch ($arg) { case "-r": $VAR = "ROOT"; break; case "-dv": case "-vd": case "--debug": case "-d": $DEBUG = TRUE; break; case "-dv": case "-vd": case "--verbose": case "-v": $VERBOSE = TRUE; break; } if (!$COMMAND && !$ALIAS && ($arg[0] == '@')) { $ALIAS = $arg; } elseif (!$COMMAND && ($arg[0] != '-')) { $COMMAND = $arg; } if (substr($arg, 0, 7) == "--root=") { $ROOT = substr($arg, 7); } } } $NONE=($ALIAS == "@none"); // If we have found the site-local Drush script, then // do not search for it again; use the environment value // we set last time. $found_script = getenv('DRUSH_FINDER_SCRIPT'); // If the @none alias is used, then we skip the Drush wrapper, // and call the Drush launcher directly. // // In this instance, we are assuming that the 'drush' that is being // called is: // // a) The global 'drush', or // b) A site-local 'drush' in a vendor/bin directory. // // In either event, the appropriate 'drush.launcher' should be right next // to this script (stored in the same directory). if (empty($found_script) && $NONE) { if (is_file(dirname(__DIR__) . "{$sep}drush.launcher")) { $found_script = dirname(__DIR__) . "{$sep}drush.launcher"; } else { fwrite(STDERR, "Could not find drush.launcher in " . dirname(__DIR__) . ". Check your installation.\n"); exit(1); } } // Check for a root option: // // drush --root=/path // // If the site root is specified via a commandline option, then we // should always use the Drush stored at this root, if there is one. // We will first check for a "wrapper" script at the root, and then // we will look for a "launcher" script in vendor/bin. if (empty($found_script) && !empty($ROOT)) { $found_script = find_wrapper_or_launcher($ROOT); if (!empty($found_script)) { chdir($ROOT); } } // If there is a .drush-use file, then its contents will // contain the path to the Drush to use. if (empty($found_script)) { if (is_file(".drush-use")) { $found_script = trim(file_get_contents(".drush-use")); } } // Look for a 'drush' wrapper or launcher at the cwd, // and in each of the directories above the cwd. If // we find one, use it. if (empty($found_script)) { $c = getcwd(); // Windows can give us lots of different strings to represent the root // directory as it often includes the drive letter. If we get the same // result from dirname() twice in a row, then we know we're at the root. $last = ''; while (!empty($c) && ($c != $last)) { $found_script = find_wrapper_or_launcher($c); if ($found_script) { chdir($c); break; } $last = $c; $c = dirname($c); } } if (!empty($found_script)) { $found_script = realpath($found_script); // Guard against errors: if we have found a "drush" script // (that is, theoretically a drush wrapper script), and // there is a "drush.launcher" script in the same directory, // then we will skip the "drush" script and use the drush launcher // instead. This is because drush "wrapper" scripts should // only ever exist at the root of a site, and there should // never be a drush "launcher" at the root of a site. // Therefore, if we find a "drush.launcher" next to a script // called "drush", we have probably found a Drush install directory, // not a site root. Adjust appropriately. Note that this // also catches the case where a drush "finder" script finds itself. if (is_file(dirname($found_script) . "{$sep}drush.launcher")) { $found_script = dirname($found_script) . "{$sep}drush.launcher"; } } // Didn't find any site-local Drush, or @use'd Drush. // Skip the Bash niceties of the launcher and proceed to drush_main() in either case: // - No script was found and we are running a Phar // - The found script *is* the Phar https://github.com/drush-ops/drush/pull/2246. $phar_path = class_exists('Phar') ? Phar::running(FALSE) : ''; if ((empty($found_script) && $phar_path) || !empty($found_script) && $found_script == $phar_path) { drush_run_main($DEBUG, $sep, "Phar detected. Proceeding to drush_main()."); } // Didn't find any site-local Drush, or @use'd Drush, or Phar. // There should be a drush.launcher in same directory as this script. if (empty($found_script)) { $found_script = dirname(__DIR__) . "{$sep}drush.launcher"; } if (drush_is_windows()) { // Sometimes we found launcher in /bin, and sometimes not. Adjust accordingly. if (strpos($found_script, 'bin')) { $found_script = dirname($found_script). $sep. 'drush.php.bat'; } else { array_unshift($arguments, dirname($found_script). $sep. 'drush.php'); $found_script = 'php'; } } // Always use pcntl_exec if it exists. $use_pcntl_exec = function_exists("pcntl_exec") && (strpos(ini_get('disable_functions'), 'pcntl_exec') === FALSE); // If we have posix_getppid, then pass in the shell pid so // that 'site-set' et. al. can work correctly. if (function_exists('posix_getppid')) { putenv("DRUSH_SHELL_PID=" . posix_getppid()); } // Set an environment variable indicating which script // the Drush finder found. If we end up re-entrantly calling // another Drush finder, then we will skip searching for // a site-local Drush, and always use the drush.launcher // found previously. This environment variable typically should // not be set by clients. putenv("DRUSH_FINDER_SCRIPT=$found_script"); // Emit a message in debug mode advertising the location of the // script we found. if ($DEBUG) { $launch_method = $use_pcntl_exec ? 'pcntl_exec' : 'proc_open'; fwrite(STDERR, "Using the Drush script found at $found_script using $launch_method\n"); } if ($use_pcntl_exec) { // Get the current environment for pnctl_exec. $env = drush_env(); // Launch the new script in the same process. // If the launch succeeds, then it will not return. $error = pcntl_exec($found_script, $arguments, $env); if (!$error) { $errno = pcntl_get_last_error(); $strerror = pcntl_strerror($errno); fwrite(STDERR, "Error has occurred executing the Drush script found at $found_script\n"); fwrite(STDERR, "(errno {$errno}) $strerror\n"); } exit(1); } else { $escaped_args = array_map(function($item) { return drush_escapeshellarg($item); }, $arguments); // Double quotes around $found_script as it can contain spaces. $cmd = drush_escapeshellarg($found_script). ' '. implode(' ', $escaped_args); if (drush_is_windows()) { // Windows requires double quotes around whole command. // @see https://bugs.php.net/bug.php?id=49139 // @see https://bugs.php.net/bug.php?id=60181 $cmd = '"'. $cmd. '"'; } $process = proc_open($cmd, array(0 => STDIN, 1 => STDOUT, 2 => STDERR), $pipes, $cwd); $proc_status = proc_get_status($process); $exit_code = proc_close($process); exit($proc_status["running"] ? $exit_code : $proc_status["exitcode"] ); } } /** * Run drush_main() and then exit. Used when we cannot hand over execution to * the launcher. * * @param bool $DEBUG * Are we in debug mode * @param string $sep * Directory separator * @param string $msg * Debug message to log before running drush_main() */ function drush_run_main($DEBUG, $sep, $msg) { // Emit a message in debug mode advertising how we proceeded. if ($DEBUG) { fwrite(STDERR, $msg. "\n"); } require __DIR__ . "{$sep}preflight.inc"; exit(drush_main()); } $error) { drush_set_error($key, $error); } drush_set_error('DRUSH_COMMAND_NOT_EXECUTABLE', dt("The drush command '!args' could not be executed.", array('!args' => $args))); } elseif (!empty($args)) { drush_set_error('DRUSH_COMMAND_NOT_FOUND', dt("The drush command '!args' could not be found. Run `drush cache-clear drush` to clear the commandfile cache if you have installed new extensions.", array('!args' => $args))); } // Set errors that occurred in the bootstrap phases. $errors = drush_get_context('DRUSH_BOOTSTRAP_ERRORS', array()); foreach ($errors as $code => $message) { drush_set_error($code, $message); } } function bootstrap_and_dispatch() { $phases = $this->bootstrap_init_phases(); $return = ''; $command_found = FALSE; _drush_bootstrap_output_prepare(); foreach ($phases as $phase) { if (drush_bootstrap_to_phase($phase)) { $command = drush_parse_command(); if (is_array($command)) { $command += $this->command_defaults(); // Insure that we have bootstrapped to a high enough // phase for the command prior to enforcing requirements. $bootstrap_result = drush_bootstrap_to_phase($command['bootstrap']); $this->enforce_requirement($command); if ($bootstrap_result && empty($command['bootstrap_errors'])) { drush_log(dt("Found command: !command (commandfile=!commandfile)", array('!command' => $command['command'], '!commandfile' => $command['commandfile'])), LogLevel::BOOTSTRAP); $command_found = TRUE; // Dispatch the command(s). $return = drush_dispatch($command); // Prevent a '1' at the end of the output. if ($return === TRUE) { $return = ''; } if (drush_get_context('DRUSH_DEBUG') && !drush_get_context('DRUSH_QUIET')) { // @todo Create version independant wrapper around Drupal timers. Use it. drush_print_timers(); } break; } } } else { break; } } if (!$command_found) { // If we reach this point, command doesn't fit requirements or we have not // found either a valid or matching command. $this->report_command_error($command); } return $return; } /** * {@inheritdoc} */ public function terminate() { } } method name. */ function bootstrap_phases(); /** * List of bootstrap phases where Drush should stop and look for commandfiles. * * This allows us to bootstrap to a minimum neccesary to find commands. * * Once a command is found, Drush will ensure a bootstrap to the phase * declared by the command. * * @return array of PHASE indexes. */ function bootstrap_init_phases(); /** * Return an array of default values that should be added * to every command (e.g. values needed in enforce_requirements(), * etc.) */ function command_defaults(); /** * Called by Drush when a command is selected, but * before it runs. This gives the Boot class an * opportunity to determine if any minimum * requirements (e.g. minimum Drupal version) declared * in the command have been met. * * @return TRUE if command is valid. $command['bootstrap_errors'] * should be populated with an array of error messages if * the command is not valid. */ function enforce_requirement(&$command); /** * Called by Drush if a command is not found, or if the * command was found, but did not meet requirements. * * The implementation in BaseBoot should be sufficient * for most cases, so this method typically will not need * to be overridden. */ function report_command_error($command); /** * This method is called during the shutdown of drush. * * @return void */ public function terminate(); } 'bootstrap_drush', DRUSH_BOOTSTRAP_DRUPAL_ROOT => 'bootstrap_drupal_root', DRUSH_BOOTSTRAP_DRUPAL_SITE => 'bootstrap_drupal_site', DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION => 'bootstrap_drupal_configuration', DRUSH_BOOTSTRAP_DRUPAL_DATABASE => 'bootstrap_drupal_database', DRUSH_BOOTSTRAP_DRUPAL_FULL => 'bootstrap_drupal_full', DRUSH_BOOTSTRAP_DRUPAL_LOGIN => 'bootstrap_drupal_login'); } /** * List of bootstrap phases where Drush should stop and look for commandfiles. * * For Drupal, we try at these bootstrap phases: * * - Drush preflight: to find commandfiles in any system location, * out of a Drupal installation. * - Drupal root: to find commandfiles based on Drupal core version. * - Drupal full: to find commandfiles defined within a Drupal directory. * * Once a command is found, Drush will ensure a bootstrap to the phase * declared by the command. * * @return array of PHASE indexes. */ function bootstrap_init_phases() { return array(DRUSH_BOOTSTRAP_DRUSH, DRUSH_BOOTSTRAP_DRUPAL_ROOT, DRUSH_BOOTSTRAP_DRUPAL_FULL); } function enforce_requirement(&$command) { parent::enforce_requirement($command); $this->drush_enforce_requirement_drupal_dependencies($command); } function report_command_error($command) { // If we reach this point, command doesn't fit requirements or we have not // found either a valid or matching command. // If no command was found check if it belongs to a disabled module. if (!$command) { $command = $this->drush_command_belongs_to_disabled_module(); } parent::report_command_error($command); } function command_defaults() { return array( 'drupal dependencies' => array(), 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_LOGIN, ); } /** * @return array of strings - paths to directories where contrib * modules can be found */ abstract function contrib_modules_paths(); /** * @return array of strings - paths to directories where contrib * themes can be found */ abstract function contrib_themes_paths(); function commandfile_searchpaths($phase, $phase_max = FALSE) { if (!$phase_max) { $phase_max = $phase; } $searchpath = array(); switch ($phase) { case DRUSH_BOOTSTRAP_DRUPAL_ROOT: $drupal_root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); $searchpath[] = $drupal_root . '/../drush'; $searchpath[] = $drupal_root . '/drush'; $searchpath[] = $drupal_root . '/sites/all/drush'; break; case DRUSH_BOOTSTRAP_DRUPAL_SITE: // If we are going to stop bootstrapping at the site, then // we will quickly add all commandfiles that we can find for // any extension associated with the site, whether it is enabled // or not. If we are, however, going to continue on to bootstrap // all the way to DRUSH_BOOTSTRAP_DRUPAL_FULL, then we will // instead wait for that phase, which will more carefully add // only those Drush commandfiles that are associated with // enabled modules. if ($phase_max < DRUSH_BOOTSTRAP_DRUPAL_FULL) { $searchpath = array_merge($searchpath, $this->contrib_modules_paths()); // Adding commandfiles located within /profiles. Try to limit to one profile for speed. Note // that Drupal allows enabling modules from a non-active profile so this logic is kinda dodgy. $cid = drush_cid_install_profile(); if ($cached = drush_cache_get($cid)) { $profile = $cached->data; $searchpath[] = "profiles/$profile/modules"; $searchpath[] = "profiles/$profile/themes"; } else { // If install_profile is not available, scan all profiles. $searchpath[] = "profiles"; $searchpath[] = "sites/all/profiles"; } $searchpath = array_merge($searchpath, $this->contrib_themes_paths()); } break; case DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION: // Nothing to do here anymore. Left for documentation. break; case DRUSH_BOOTSTRAP_DRUPAL_FULL: // Add enabled module paths, excluding the install profile. Since we are bootstrapped, // we can use the Drupal API. $ignored_modules = drush_get_option_list('ignored-modules', array()); $cid = drush_cid_install_profile(); if ($cached = drush_cache_get($cid)) { $ignored_modules[] = $cached->data; } foreach (array_diff(drush_module_list(), $ignored_modules) as $module) { $filepath = drupal_get_path('module', $module); if ($filepath && $filepath != '/') { $searchpath[] = $filepath; } } // Check all enabled themes including non-default and non-admin. foreach (drush_theme_list() as $key => $value) { $searchpath[] = drupal_get_path('theme', $key); } break; } return $searchpath; } /** * Check if the given command belongs to a disabled module. * * @return array * Array with a command-like bootstrap error or FALSE if Drupal was not * bootstrapped fully or the command does not belong to a disabled module. */ function drush_command_belongs_to_disabled_module() { if (drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { _drush_find_commandfiles(DRUSH_BOOTSTRAP_DRUPAL_SITE, DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION); drush_get_commands(TRUE); $commands = drush_get_commands(); $arguments = drush_get_arguments(); $command_name = array_shift($arguments); if (isset($commands[$command_name])) { // We found it. Load its module name and set an error. if (is_array($commands[$command_name]['drupal dependencies']) && count($commands[$command_name]['drupal dependencies'])) { $modules = implode(', ', $commands[$command_name]['drupal dependencies']); } else { // The command does not define Drupal dependencies. Derive them. $command_files = commandfiles_cache()->get(); $command_path = $commands[$command_name]['path'] . DIRECTORY_SEPARATOR . $commands[$command_name]['commandfile'] . '.drush.inc'; $modules = array_search($command_path, $command_files); } return array( 'bootstrap_errors' => array( 'DRUSH_COMMAND_DEPENDENCY_ERROR' => dt('Command !command needs the following extension(s) enabled to run: !dependencies.', array( '!command' => $command_name, '!dependencies' => $modules, )), ), ); } } return FALSE; } /** * Check that a command has its declared dependencies available or have no * dependencies. * * @param $command * Command to check. Any errors will be added to the 'bootstrap_errors' element. * * @return * TRUE if command is valid. */ function drush_enforce_requirement_drupal_dependencies(&$command) { // If the command bootstrap is DRUSH_BOOTSTRAP_MAX, then we will // allow the requirements to pass if we have not successfully // bootstrapped Drupal. The combination of DRUSH_BOOTSTRAP_MAX // and 'drupal dependencies' indicates that the drush command // will use the dependent modules only if they are available. if ($command['bootstrap'] == DRUSH_BOOTSTRAP_MAX) { // If we have not bootstrapped, then let the dependencies pass; // if we have bootstrapped, then enforce them. if (drush_get_context('DRUSH_BOOTSTRAP_PHASE') < DRUSH_BOOTSTRAP_DRUPAL_FULL) { return TRUE; } } // If there are no drupal dependencies, then do nothing if (!empty($command['drupal dependencies'])) { foreach ($command['drupal dependencies'] as $dependency) { drush_include_engine('drupal', 'environment'); if(!drush_module_exists($dependency)) { $command['bootstrap_errors']['DRUSH_COMMAND_DEPENDENCY_ERROR'] = dt('Command !command needs the following modules installed/enabled to run: !dependencies.', array('!command' => $command['command'], '!dependencies' => implode(', ', $command['drupal dependencies']))); return FALSE; } } } return TRUE; } /** * Validate the DRUSH_BOOTSTRAP_DRUPAL_ROOT phase. * * In this function, we will check if a valid Drupal directory is available. * We also determine the value that will be stored in the DRUSH_DRUPAL_ROOT * context and DRUPAL_ROOT constant if it is considered a valid option. */ function bootstrap_drupal_root_validate() { $drupal_root = drush_get_context('DRUSH_SELECTED_DRUPAL_ROOT'); if (empty($drupal_root)) { return drush_bootstrap_error('DRUSH_NO_DRUPAL_ROOT', dt("A Drupal installation directory could not be found")); } if (!$signature = drush_valid_root($drupal_root)) { return drush_bootstrap_error('DRUSH_INVALID_DRUPAL_ROOT', dt("The directory !drupal_root does not contain a valid Drupal installation", array('!drupal_root' => $drupal_root))); } $version = drush_drupal_version($drupal_root); $major_version = drush_drupal_major_version($drupal_root); if ($major_version <= 5) { return drush_set_error('DRUSH_DRUPAL_VERSION_UNSUPPORTED', dt('Drush !drush_version does not support Drupal !major_version.', array('!drush_version' => DRUSH_VERSION, '!major_version' => $major_version))); } drush_bootstrap_value('drupal_root', $drupal_root); define('DRUSH_DRUPAL_SIGNATURE', $signature); return TRUE; } /** * Bootstrap Drush with a valid Drupal Directory. * * In this function, the pwd will be moved to the root * of the Drupal installation. * * The DRUSH_DRUPAL_ROOT context, DRUSH_DRUPAL_CORE context, DRUPAL_ROOT, and the * DRUSH_DRUPAL_CORE constants are populated from the value that we determined during * the validation phase. * * We also now load the drushrc.php for this specific Drupal site. * We can now include files from the Drupal Tree, and figure * out more context about the platform, such as the version of Drupal. */ function bootstrap_drupal_root() { // Load the config options from Drupal's /drush and sites/all/drush directories. drush_load_config('drupal'); $drupal_root = drush_set_context('DRUSH_DRUPAL_ROOT', drush_bootstrap_value('drupal_root')); chdir($drupal_root); $version = drush_drupal_version(); $major_version = drush_drupal_major_version(); $core = $this->bootstrap_drupal_core($drupal_root); // DRUSH_DRUPAL_CORE should point to the /core folder in Drupal 8+ or to DRUPAL_ROOT // in prior versions. drush_set_context('DRUSH_DRUPAL_CORE', $core); define('DRUSH_DRUPAL_CORE', $core); _drush_preflight_global_options(); drush_log(dt("Initialized Drupal !version root directory at !drupal_root", array("!version" => $version, '!drupal_root' => $drupal_root)), LogLevel::BOOTSTRAP); } /** * VALIDATE the DRUSH_BOOTSTRAP_DRUPAL_SITE phase. * * In this function we determine the URL used for the command, * and check for a valid settings.php file. * * To do this, we need to set up the $_SERVER environment variable, * to allow us to use conf_path to determine what Drupal will load * as a configuration file. */ function bootstrap_drupal_site_validate() { // Define the selected conf path as soon as we have identified that // we have selected a Drupal site. Drush used to set this context // during the drush_bootstrap_drush phase. $drush_uri = _drush_bootstrap_selected_uri(); drush_set_context('DRUSH_SELECTED_DRUPAL_SITE_CONF_PATH', drush_conf_path($drush_uri)); $this->bootstrap_drupal_site_setup_server_global($drush_uri); return $this->bootstrap_drupal_site_validate_settings_present(); } /** * Set up the $_SERVER globals so that Drupal will see the same values * that it does when serving pages via the web server. */ function bootstrap_drupal_site_setup_server_global($drush_uri) { // Fake the necessary HTTP headers that Drupal needs: if ($drush_uri) { $drupal_base_url = parse_url($drush_uri); // If there's no url scheme set, add http:// and re-parse the url // so the host and path values are set accurately. if (!array_key_exists('scheme', $drupal_base_url)) { $drush_uri = 'http://' . $drush_uri; $drupal_base_url = parse_url($drush_uri); } // Fill in defaults. $drupal_base_url += array( 'path' => '', 'host' => NULL, 'port' => NULL, ); $_SERVER['HTTP_HOST'] = $drupal_base_url['host']; if ($drupal_base_url['scheme'] == 'https') { $_SERVER['HTTPS'] = 'on'; } if ($drupal_base_url['port']) { $_SERVER['HTTP_HOST'] .= ':' . $drupal_base_url['port']; } $_SERVER['SERVER_PORT'] = $drupal_base_url['port']; $_SERVER['REQUEST_URI'] = $drupal_base_url['path'] . '/'; } else { $_SERVER['HTTP_HOST'] = 'default'; $_SERVER['REQUEST_URI'] = '/'; } $_SERVER['PHP_SELF'] = $_SERVER['REQUEST_URI'] . 'index.php'; $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF']; $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; $_SERVER['REQUEST_METHOD'] = 'GET'; $_SERVER['SERVER_SOFTWARE'] = NULL; $_SERVER['HTTP_USER_AGENT'] = NULL; $_SERVER['SCRIPT_FILENAME'] = DRUPAL_ROOT . '/index.php'; } /** * Validate that the Drupal site has all of the settings that it * needs to operated. */ function bootstrap_drupal_site_validate_settings_present() { $site = drush_bootstrap_value('site', $_SERVER['HTTP_HOST']); $conf_path = drush_bootstrap_value('conf_path', $this->conf_path(TRUE, TRUE)); $conf_file = "$conf_path/settings.php"; if (!file_exists($conf_file)) { return drush_bootstrap_error('DRUPAL_SITE_SETTINGS_NOT_FOUND', dt("Could not find a Drupal settings.php file at !file.", array('!file' => $conf_file))); } return TRUE; } /** * Called by bootstrap_drupal_site to do the main work * of the drush drupal site bootstrap. */ function bootstrap_do_drupal_site() { $drush_uri = drush_get_context('DRUSH_SELECTED_URI'); drush_set_context('DRUSH_URI', $drush_uri); $site = drush_set_context('DRUSH_DRUPAL_SITE', drush_bootstrap_value('site')); $conf_path = drush_set_context('DRUSH_DRUPAL_SITE_ROOT', drush_bootstrap_value('conf_path')); drush_log(dt("Initialized Drupal site !site at !site_root", array('!site' => $site, '!site_root' => $conf_path)), LogLevel::BOOTSTRAP); _drush_preflight_global_options(); } /** * Initialize a site on the Drupal root. * * We now set various contexts that we determined and confirmed to be valid. * Additionally we load an optional drushrc.php file in the site directory. */ function bootstrap_drupal_site() { drush_load_config('site'); $this->bootstrap_do_drupal_site(); } /** * Initialize and load the Drupal configuration files. * * We process and store a normalized set of database credentials * from the loaded configuration file, so we can validate them * and access them easily in the future. * * Also override Drupal variables as per --variables option. */ function bootstrap_drupal_configuration() { global $conf; $override = array( 'dev_query' => FALSE, // Force Drupal6 not to store queries since we are not outputting them. 'cron_safe_threshold' => 0, // Don't run poormanscron during Drush request (D7+). ); $current_override = drush_get_option_list('variables'); foreach ($current_override as $name => $value) { if (is_numeric($name) && (strpos($value, '=') !== FALSE)) { list($name, $value) = explode('=', $value, 2); } $override[$name] = $value; } $conf = is_array($conf) ? array_merge($conf, $override) : $conf; } /** * Validate the DRUSH_BOOTSTRAP_DRUPAL_DATABASE phase * * Attempt to make a working database connection using the * database credentials that were loaded during the previous * phase. */ function bootstrap_drupal_database_validate() { if (!drush_valid_db_credentials()) { return drush_bootstrap_error('DRUSH_DRUPAL_DB_ERROR'); } return TRUE; } /** * Test to see if the Drupal database has a specified * table or tables. * * This is a bootstrap helper function designed to be called * from the bootstrap_drupal_database_validate() methods of * derived DrupalBoot classes. If a database exists, but is * empty, then the Drupal database bootstrap will fail. To * prevent this situation, we test for some table that is needed * in an ordinary bootstrap, and return FALSE from the validate * function if it does not exist, so that we do not attempt to * start the database bootstrap. * * Note that we must manually do our own prefix testing here, * because the existing wrappers we have for handling prefixes * depend on bootstrapping to the "database" phase, and therefore * are not available to validate this same phase. * * @param $required_tables * Array of table names, or string with one table name * * @return TRUE if all tables in input parameter exist in * the database. */ function bootstrap_drupal_database_has_table($required_tables) { try { $sql = drush_sql_get_class(); $spec = $sql->db_spec(); $prefix = isset($spec['prefix']) ? $spec['prefix'] : NULL; if (!is_array($prefix)) { $prefix = array('default' => $prefix); } $tables = $sql->listTables(); foreach ((array)$required_tables as $required_table) { $prefix_key = array_key_exists($required_table, $prefix) ? $required_table : 'default'; if (!in_array($prefix[$prefix_key] . $required_table, $tables)) { return FALSE; } } } catch (Exception $e) { // Usually the checks above should return a result without // throwing an exception, but we'll catch any that are // thrown just in case. return FALSE; } return TRUE; } /** * Boostrap the Drupal database. */ function bootstrap_drupal_database() { // We presume that our derived classes will connect and then // either fail, or call us via parent:: drush_log(dt("Successfully connected to the Drupal database."), LogLevel::BOOTSTRAP); } /** * Attempt to load the full Drupal system. */ function bootstrap_drupal_full() { drush_include_engine('drupal', 'environment'); $this->add_logger(); // Write correct install_profile to cache as needed. Used by _drush_find_commandfiles(). $cid = drush_cid_install_profile(); $install_profile = $this->get_profile(); if ($cached_install_profile = drush_cache_get($cid)) { // We have a cached profile. Check it for correctness and save new value if needed. if ($cached_install_profile->data != $install_profile) { drush_cache_set($cid, $install_profile); } } else { // No cached entry so write to cache. drush_cache_set($cid, $install_profile); } _drush_log_drupal_messages(); } /** * Log into the bootstrapped Drupal site with a specific * username or user id. */ function bootstrap_drupal_login() { $uid_or_name = drush_set_context('DRUSH_USER', drush_get_option('user', 0)); $userversion = drush_user_get_class(); if (!$account = $userversion->load_by_uid($uid_or_name)) { if (!$account = $userversion->load_by_name($uid_or_name)) { if (is_numeric($uid_or_name)) { $message = dt('Could not login with user ID !user.', array('!user' => $uid_or_name)); if ($uid_or_name === 0) { $message .= ' ' . dt('This is typically caused by importing a MySQL database dump from a faulty tool which re-numbered the anonymous user ID in the users table. See !link for help recovering from this situation.', array('!link' => 'http://drupal.org/node/1029506')); } } else { $message = dt('Could not login with user account `!user\'.', array('!user' => $uid_or_name)); } return drush_set_error('DRUPAL_USER_LOGIN_FAILED', $message); } } $userversion->setCurrentUser($account); _drush_log_drupal_messages(); } } conf_path() . '/modules', 'sites/all/modules', ); } function contrib_themes_paths() { return array( $this->conf_path() . '/themes', 'sites/all/themes', ); } function bootstrap_drupal_core($drupal_root) { define('DRUPAL_ROOT', $drupal_root); require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; $core = DRUPAL_ROOT; return $core; } function bootstrap_drupal_database_validate() { return parent::bootstrap_drupal_database_validate() && $this->bootstrap_drupal_database_has_table('cache'); } function bootstrap_drupal_database() { drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE); parent::bootstrap_drupal_database(); } function bootstrap_drupal_configuration() { drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); parent::bootstrap_drupal_configuration(); } function bootstrap_drupal_full() { if (!drush_get_context('DRUSH_QUIET', FALSE)) { ob_start(); } drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); if (!drush_get_context('DRUSH_QUIET', FALSE)) { ob_end_clean(); } // Unset drupal error handler and restore drush's one. restore_error_handler(); parent::bootstrap_drupal_full(); } } conf_path() . '/modules', 'sites/all/modules', ); } function contrib_themes_paths() { return array( $this->conf_path() . '/themes', 'sites/all/themes', ); } function bootstrap_drupal_core($drupal_root) { define('DRUPAL_ROOT', $drupal_root); require_once DRUPAL_ROOT . '/includes/bootstrap.inc'; $core = DRUPAL_ROOT; return $core; } function bootstrap_drupal_database_validate() { return parent::bootstrap_drupal_database_validate() && $this->bootstrap_drupal_database_has_table('blocked_ips'); } function bootstrap_drupal_database() { drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE); parent::bootstrap_drupal_database(); } function bootstrap_drupal_configuration() { drupal_bootstrap(DRUPAL_BOOTSTRAP_CONFIGURATION); // Unset drupal error handler and restore drush's one. restore_error_handler(); parent::bootstrap_drupal_configuration(); } function bootstrap_drupal_full() { if (!drush_get_context('DRUSH_QUIET', FALSE)) { ob_start(); } drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL); if (!drush_get_context('DRUSH_QUIET', FALSE)) { ob_end_clean(); } parent::bootstrap_drupal_full(); } } getSitePath(); } if (!isset($site_path) || empty($site_path)) { $site_path = DrupalKernel::findSitePath($request, $require_settings); } return $site_path; } function add_logger() { // If we're running on Drupal 8 or later, we provide a logger which will send // output to drush_log(). This should catch every message logged through every // channel. $container = \Drupal::getContainer(); $parser = $container->get('logger.log_message_parser'); $drushLogger = drush_get_context('DRUSH_LOG_CALLBACK'); $logger = new \Drush\Log\DrushLog($parser, $drushLogger); $container->get('logger.factory')->addLogger($logger); } function contrib_modules_paths() { return array( $this->conf_path() . '/modules', 'sites/all/modules', 'modules', ); } /** * @return array of strings - paths to directories where contrib * themes can be found */ function contrib_themes_paths() { return array( $this->conf_path() . '/themes', 'sites/all/themes', 'themes', ); } function bootstrap_drupal_core($drupal_root) { $core = DRUPAL_ROOT . '/core'; return $core; } function bootstrap_drupal_database_validate() { return parent::bootstrap_drupal_database_validate() && $this->bootstrap_drupal_database_has_table('key_value'); } function bootstrap_drupal_database() { // D8 omits this bootstrap level as nothing special needs to be done. parent::bootstrap_drupal_database(); } function bootstrap_drupal_configuration() { $this->request = Request::createFromGlobals(); $classloader = drush_drupal_load_autoloader(DRUPAL_ROOT); // @todo - use Request::create() and then no need to set PHP superglobals $kernelClass = new \ReflectionClass('\Drupal\Core\DrupalKernel'); if ($kernelClass->hasMethod('addServiceModifier')) { $this->kernel = DrupalKernel::createFromRequest($this->request, $classloader, 'prod'); } else { $this->kernel = DrushDrupalKernel::createFromRequest($this->request, $classloader, 'prod'); } // @see Drush\Drupal\DrupalKernel::addServiceModifier() $this->kernel->addServiceModifier(new DrushServiceModifier()); // Unset drupal error handler and restore Drush's one. restore_error_handler(); // Disable automated cron if the module is enabled. $GLOBALS['config']['automated_cron.settings']['interval'] = 0; parent::bootstrap_drupal_configuration(); } function bootstrap_drupal_full() { drush_log(dt('About to bootstrap the Drupal 8 Kernel.'), LogLevel::DEBUG); // TODO: do we need to do ob_start any longer? if (!drush_get_context('DRUSH_QUIET', FALSE)) { ob_start(); } $this->kernel->boot(); $this->kernel->prepareLegacyRequest($this->request); if (!drush_get_context('DRUSH_QUIET', FALSE)) { ob_end_clean(); } drush_log(dt('Finished bootstraping the Drupal 8 Kernel.'), LogLevel::DEBUG); parent::bootstrap_drupal_full(); // Get a list of the modules to ignore $ignored_modules = drush_get_option_list('ignored-modules', array()); // We have to get the service command list from the container, because // it is constructed in an indirect way during the container initialization. // The upshot is that the list of console commands is not available // until after $kernel->boot() is called. $container = \Drupal::getContainer(); $serviceCommandlist = $container->get('drush.service.consolecommands'); foreach ($serviceCommandlist->getCommandList() as $command) { if (!$this->commandIgnored($command, $ignored_modules)) { drush_log(dt('Add a command: !name', ['!name' => $command->getName()]), LogLevel::DEBUG); annotationcommand_adapter_cache_module_console_commands($command); } } // Do the same thing with the annotation commands. $serviceCommandlist = $container->get('drush.service.consolidationcommands'); foreach ($serviceCommandlist->getCommandList() as $commandhandler) { if (!$this->commandIgnored($commandhandler, $ignored_modules)) { drush_log(dt('Add a commandhandler: !name', ['!name' => get_class($commandhandler)]), LogLevel::DEBUG); annotationcommand_adapter_cache_module_service_commands($commandhandler); } } } public function commandIgnored($command, $ignored_modules) { if (empty($ignored_modules)) { return false; } $ignored_regex = '#\\\\(' . implode('|', $ignored_modules) . ')\\\\#'; $class = new \ReflectionClass($command); $commandNamespace = $class->getNamespaceName(); return preg_match($ignored_regex, $commandNamespace); } /** * {@inheritdoc} */ public function terminate() { parent::terminate(); if ($this->kernel) { $response = Response::create(''); $this->kernel->terminate($this->request, $response); } } } '_drush_bootstrap_drush', ); } function bootstrap_init_phases() { return array(DRUSH_BOOTSTRAP_DRUSH); } function command_defaults() { return array( // TODO: Historically, commands that do not explicitly specify // their bootstrap level default to DRUSH_BOOTSTRAP_DRUPAL_LOGIN. // This isn't right any more, but we can't just change this to // DRUSH_BOOTSTRAP_DRUSH, or we will start running commands that // needed a full bootstrap with no bootstrap, and that won't work. // For now, we will continue to force this to 'login'. Any command // that does not declare 'bootstrap' is declaring that it is a Drupal // command. 'bootstrap' => DRUSH_BOOTSTRAP_DRUPAL_LOGIN, ); } } bin = $bin; $this->directory = $this->cacheDirectory(); } /** * Returns the cache directory for the given bin. * * @param string $bin */ function cacheDirectory($bin = NULL) { $bin = $bin ? $bin : $this->bin; return drush_directory_cache($bin); } function get($cid) { $cids = array($cid); $cache = $this->getMultiple($cids); return reset($cache); } function getMultiple(&$cids) { try { $cache = array(); foreach ($cids as $cid) { $filename = $this->getFilePath($cid); if (!file_exists($filename)) throw new \Exception; $item = $this->readFile($filename); if ($item) { $cache[$cid] = $item; } } $cids = array_diff($cids, array_keys($cache)); return $cache; } catch (\Exception $e) { return array(); } } /** * Returns the contents of the given filename unserialized. * * @param string $filename * Absolute path to filename to read contents from. */ function readFile($filename) { $item = file_get_contents($filename); return $item ? unserialize($item) : FALSE; } function set($cid, $data, $expire = DRUSH_CACHE_PERMANENT) { $created = time(); $cache = new \stdClass; $cache->cid = $cid; $cache->data = is_object($data) ? clone $data : $data; $cache->created = $created; if ($expire == DRUSH_CACHE_TEMPORARY) { $cache->expire = $created + 2591999; } // Expire time is in seconds if less than 30 days, otherwise is a timestamp. elseif ($expire != DRUSH_CACHE_PERMANENT && $expire < 2592000) { $cache->expire = $created + $expire; } else { $cache->expire = $expire; } // Ensure the cache directory still exists, in case a backend process // cleared the cache after the cache was initialized. drush_mkdir($this->directory); $filename = $this->getFilePath($cid); return $this->writeFile($filename, $cache); } /** * Serializes data and write it to the given filename. * * @param string $filename * Absolute path to filename to write cache data. * @param $cache * Cache data to serialize and write to $filename. */ function writeFile($filename, $cache) { return file_put_contents($filename, serialize($cache)); } function clear($cid = NULL, $wildcard = FALSE) { $bin_dir = $this->cacheDirectory(); $files = array(); if (empty($cid)) { drush_delete_dir($bin_dir, TRUE); } else { if ($wildcard) { if ($cid == '*') { drush_delete_dir($bin_dir, TRUE); } else { $matches = drush_scan_directory($bin_dir, "/^$cid/", array('.', '..')); $files = $files + array_keys($matches); } } else { $files[] = $this->getFilePath($cid); } foreach ($files as $f) { if (file_exists($f)) { unlink($f); } } } } function isEmpty() { $files = drush_scan_directory($this->directory, "//", array('.', '..')); return empty($files); } /** * Converts a cache id to a full path. * * @param $cid * The cache ID of the data to retrieve. * * @return * The full path to the cache file. */ protected function getFilePath($cid) { return $this->directory . '/' . str_replace(array(':'), '.', $cid) . self::EXTENSION; } } cache = array(); $this->deferred = array(); } function get() { return $this->cache; } function deferred() { return $this->deferred; } function sort() { ksort($this->cache); } function add($commandfile) { $load_command = FALSE; $module = basename($commandfile); $module = preg_replace('/\.*drush[0-9]*\.inc/', '', $module); $module_versionless = preg_replace('/\.d([0-9]+)$/', '', $module); if (!isset($this->cache[$module_versionless])) { $drupal_version = ''; if (preg_match('/\.d([0-9]+)$/', $module, $matches)) { $drupal_version = $matches[1]; } if (empty($drupal_version)) { $load_command = TRUE; } else { if (function_exists('drush_drupal_major_version') && ($drupal_version == drush_drupal_major_version())) { $load_command = TRUE; } else { // Signal that we should try again on // the next bootstrap phase. $this->deferred[$module] = $commandfile; } } if ($load_command) { $this->cache[$module_versionless] = $commandfile; require_once $commandfile; unset($this->deferred[$module]); } } return $load_command; } } arguments = $arguments; $this->options = $options; // If a command name is provided as a parameter, then push // it onto the front of the arguments list as a service if ($command) { $this->arguments = array_merge( [ 'command' => $command ], $this->arguments ); } // Is it interactive, or is it not interactive? // Call drush_get_option() here if value not passed in? $this->interactive = $interactive; } /** * {@inheritdoc} */ public function getFirstArgument() { return reset($this->arguments); } /** * {@inheritdoc} */ public function hasParameterOption($values, $onlyParams = false) { $values = (array) $values; foreach ($values as $value) { if (array_key_exists($value, $this->options)) { return true; } } return false; } /** * {@inheritdoc} */ public function getParameterOption($values, $default = false, $onlyParams = false) { $values = (array) $values; foreach ($values as $value) { if (array_key_exists($value, $this->options)) { return $this->getOption($value); } } return $default; } /** * {@inheritdoc} */ public function bind(InputDefinition $definition) { // no-op: this class exists to avoid validation } /** * {@inheritdoc} */ public function validate() { // no-op: this class exists to avoid validation } /** * {@inheritdoc} */ public function getArguments() { return $this->arguments; } /** * {@inheritdoc} */ public function getArgument($name) { // TODO: better to throw if an argument that does not exist is requested? return isset($this->arguments[$name]) ? $this->arguments[$name] : ''; } /** * {@inheritdoc} */ public function setArgument($name, $value) { $this->arguments[$name] = $value; } /** * {@inheritdoc} */ public function hasArgument($name) { return isset($this->arguments[$name]); } /** * {@inheritdoc} */ public function getOptions() { return $this->options; } /** * {@inheritdoc} */ public function getOption($name) { return $this->options[$name]; } /** * {@inheritdoc} */ public function setOption($name, $value) { $this->options[$name] = $value; } /** * {@inheritdoc} */ public function hasOption($name) { return isset($this->options[$name]); } /** * {@inheritdoc} */ public function isInteractive() { return $this->interactive; } /** * {@inheritdoc} */ public function setInteractive($interactive) { $this->interactive = $interactive; } } commandList[] = $command; } public function getCommandList() { return $this->commandList; } } NULL, 'redirect-port' => NULL]) { // Redispatch if called against a remote-host so a browser is started on the // the *local* machine. $alias = drush_get_context('DRUSH_TARGET_SITE_ALIAS'); if (drush_sitealias_is_remote_site($alias)) { $site_record = drush_sitealias_get_record($alias); $return = drush_invoke_process($site_record, 'browse', array($path), drush_redispatch_get_options(), array('integrate' => TRUE)); if ($return['error_status']) { return drush_set_error('Unable to execute browse command on remote alias.'); } else { $link = $return['object']; } } else { if (!drush_bootstrap(DRUSH_BOOTSTRAP_DRUPAL_FULL)) { // Fail gracefully if unable to bootstrap Drupal. drush_bootstrap() has // already logged an error. return FALSE; } $link = drush_url($path, array('absolute' => TRUE)); } drush_start_browser($link); return $link; } /* * An argument completion provider */ static function complete() { return ['values' => ['admin', 'admin/content', 'admin/reports', 'admin/structure', 'admin/people', 'admin/modules', 'admin/config']]; } }printed) { return; } $this->printed = true; $annotationData = $commandData->annotationData(); $commandName = $annotationData['command']; // For some reason, Drush help uses drush_invoke_process to call helpsingle if ($commandName == 'helpsingle') { return; } drush_log(dt('Displaying Druplicon for "!command" command.', array('!command' => $commandName))); if ($commandData->input()->getOption('druplicon')) { $misc_dir = DRUSH_BASE_PATH . '/misc'; if (drush_get_context('DRUSH_NOCOLOR')) { $content = file_get_contents($misc_dir . '/druplicon-no_color.txt'); } else { $content = file_get_contents($misc_dir . '/druplicon-color.txt'); } drush_print($content); } } } 'table', 'fields' => '']) { $outputData = [ 'en' => [ 'first' => 'One', 'second' => 'Two', 'third' => 'Three' ], 'de' => [ 'first' => 'Eins', 'second' => 'Zwei', 'third' => 'Drei' ], 'jp' => [ 'first' => 'Ichi', 'second' => 'Ni', 'third' => 'San' ], 'es' => [ 'first' => 'Uno', 'second' => 'Dos', 'third' => 'Tres' ], ]; return new RowsOfFields($outputData); } /** * Demonstrate an alter hook with an option * * @hook alter example:table * @option french Add a row with French numbers. * @usage example:formatters --french */ public function alterFormatters($result, CommandData $commandData) { if ($commandData->input()->getOption('french')) { $result['fr'] = [ 'first' => 'Un', 'second' => 'Deux', 'third' => 'Trois' ]; } return $result; } } wrap to TRUE if a db-prefix is set with drush. */ protected function setWrap() { $this->wrap = $wrap_table_name = (bool) drush_get_option('db-prefix'); } /** * Sanitize the database by removed and obfuscating user data. * * @command sql-sanitize * * @todo "drush dependencies" array('sqlsync') * * @bootstrap DRUSH_BOOTSTRAP_NONE * @description Run sanitization operations on the current database. * @option db-prefix Enable replacement of braces in sanitize queries. * @option db-url A Drupal 6 style database URL. E.g., * mysql://root:pass@127.0.0.1/db * @option sanitize-email The pattern for test email addresses in the * sanitization operation, or "no" to keep email addresses unchanged. May * contain replacement patterns %uid, %mail or %name. Example value: * user+%uid@localhost * @option sanitize-password The password to assign to all accounts in the * sanitization operation, or "no" to keep passwords unchanged. Example * value: password * @option whitelist-fields A comma delimited list of fields exempt from sanitization. * @aliases sqlsan * @usage drush sql-sanitize --sanitize-password=no * Sanitize database without modifying any passwords. * @usage drush sql-sanitize --whitelist-fields=field_biography,field_phone_number * Sanitizes database but exempts two user fields from modification. * @see hook_drush_sql_sync_sanitize() for adding custom sanitize routines. */ public function sqlSanitize($options = [ 'db-prefix' => FALSE, 'db-url' => '', 'sanitize-email' => '', 'sanitize-password' => '', 'whitelist-fields' => '', ]) { drush_sql_bootstrap_further(); if ($options['db-prefix']) { drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_DATABASE); } // Drush itself implements this via sql_drush_sql_sync_sanitize(). drush_command_invoke_all('drush_sql_sync_sanitize', 'default'); $operations = drush_get_context('post-sync-ops'); if (!empty($operations)) { if (!drush_get_context('DRUSH_SIMULATE')) { $messages = _drush_sql_get_post_sync_messages(); if ($messages) { drush_print(); drush_print($messages); } } $queries = array_column($operations, 'query'); $sanitize_query = implode(" ", $queries); } if (!drush_confirm(dt('Do you really want to sanitize the current database?'))) { return drush_user_abort(); } if ($sanitize_query) { $sql = drush_sql_get_class(); $sanitize_query = $sql->query_prefix($sanitize_query); $result = $sql->query($sanitize_query); if (!$result) { throw new \Exception(dt('Sanitize query failed.')); } } } /** * Performs database sanitization. * * @param int $major_version * E.g., 6, 7, or 8. */ public function doSanitize($major_version) { $this->setWrap(); $this->sanitizeSessions(); if ($major_version == 8) { $this->sanitizeComments(); $this->sanitizeUserFields(); } } /** * Sanitize string fields associated with the user. * * We've got to do a good bit of SQL-foo here because Drupal services are * not yet available. */ public function sanitizeUserFields() { /** @var SqlBase $sql_class */ $sql_class = drush_sql_get_class(); $tables = $sql_class->listTables(); $whitelist_fields = (array) explode(',', drush_get_option('whitelist-fields')); foreach ($tables as $table) { if (strpos($table, 'user__field_') === 0) { $field_name = substr($table, 6, strlen($table)); if (in_array($field_name, $whitelist_fields)) { continue; } $output = $this->query("SELECT data FROM config WHERE name = 'field.field.user.user.$field_name';"); $field_config = unserialize($output[0]); $field_type = $field_config['field_type']; $randomizer = new Random(); switch ($field_type) { case 'email': $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->name(10) . '@example.com'); break; case 'string': $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->name(255)); break; case 'string_long': $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->sentences(1)); break; case 'telephone': $this->sanitizeTableColumn($table, $field_name . '_value', '15555555555'); break; case 'text': $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->paragraphs(2)); break; case 'text_long': $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->paragraphs(10)); break; case 'text_with_summary': $this->sanitizeTableColumn($table, $field_name . '_value', $randomizer->paragraphs(2)); $this->sanitizeTableColumn($table, $field_name . '_summary', $randomizer->name(255)); break; } } } } /** * Replaces all values in given table column with the specified value. * * @param string $table * The database table name. * @param string $column * The database column to be updated. * @param $value * The new value. */ public function sanitizeTableColumn($table, $column, $value) { $table_name_wrapped = $this->wrapTableName($table); $sql = "UPDATE $table_name_wrapped SET $column='$value';"; drush_sql_register_post_sync_op($table.$column, dt("Replaces all values in $table table with the same random long string."), $sql); } /** * Truncates the session table. */ public function sanitizeSessions() { // Seems quite portable (SQLite?) - http://en.wikipedia.org/wiki/Truncate_(SQL) $table_name = $this->wrapTableName('sessions'); $sql_sessions = "TRUNCATE TABLE $table_name;"; drush_sql_register_post_sync_op('sessions', dt('Truncate Drupal\'s sessions table'), $sql_sessions); } /** * Sanitizes comments_field_data table. */ public function sanitizeComments() { $comments_enabled = $this->query("SHOW TABLES LIKE 'comment_field_data';"); if (!$comments_enabled) { return; } $comments_table = $this->wrapTableName('comment_field_data'); $sql_comments = "UPDATE $comments_table SET name='Anonymous', mail='', homepage='http://example.com' WHERE uid = 0;"; drush_sql_register_post_sync_op('anon_comments', dt('Remove names and email addresses from anonymous user comments.'), $sql_comments); $sql_comments = "UPDATE $comments_table SET name=CONCAT('User', `uid`), mail=CONCAT('user+', `uid`, '@example.com'), homepage='http://example.com' WHERE uid <> 0;"; drush_sql_register_post_sync_op('auth_comments', dt('Replace names and email addresses from authenticated user comments.'), $sql_comments); } /** * Wraps a table name in brackets if a database prefix is being used. * * @param string $table_name * The name of the database table. * * @return string * The (possibly wrapped) table name. */ public function wrapTableName($table_name) { if ($this->wrap) { $processed = '{' . $table_name . '}'; } else { $processed = $table_name; } return $processed; } /** * Executes a sql command using drush sqlq and returns the output. * * @param string $query * The SQL query to execute. Must end in a semicolon! * * @return string * The output of the query. */ protected function query($query) { $current = drush_get_context('DRUSH_SIMULATE'); drush_set_context('DRUSH_SIMULATE', FALSE); $sql = drush_sql_get_class(); $success = $sql->query($query); $output = drush_shell_exec_output(); drush_set_context('DRUSH_SIMULATE', $current); return $output; } } '', 'format' => 'table', 'fields' => '']) { $data = _core_site_status_table($options['project']); return new AssociativeList($data); } } initializeSettings($request); return $kernel; } /** * Add a service modifier to the container builder. * * The container is not compiled until $kernel->boot(), so there is a chance * for clients to add compiler passes et. al. before then. */ public function addServiceModifier(ServiceModifierInterface $serviceModifier) { drush_log(dt("add service modifier"), LogLevel::DEBUG); $this->serviceModifiers[] = $serviceModifier; } /** * @inheritdoc */ protected function getContainerBuilder() { drush_log(dt("get container builder"), LogLevel::DEBUG); $container = parent::getContainerBuilder(); foreach ($this->serviceModifiers as $serviceModifier) { $serviceModifier->alter($container); } return $container; } /** * Initializes the service container. * * @return \Symfony\Component\DependencyInjection\ContainerInterface */ protected function initializeContainer() { if (empty($this->moduleList) && !$this->containerNeedsRebuild) { $container_definition = $this->getCachedContainerDefinition(); foreach ($this->serviceModifiers as $serviceModifier) { if (!$serviceModifier->check($container_definition)) { $this->invalidateContainer(); break; } } } return parent::initializeContainer(); } } register('drush.service.consolecommands', 'Drush\Command\ServiceCommandlist'); $container->addCompilerPass(new FindCommandsCompilerPass('drush.service.consolecommands', 'drush.command')); $container->register('drush.service.consolidationcommands', 'Drush\Command\ServiceCommandlist'); $container->addCompilerPass(new FindCommandsCompilerPass('drush.service.consolidationcommands', 'consolidation.commandhandler')); } /** * Checks existing service definitions for the presence of modification. * * @param $container_definition * Cached container definition * @return bool */ public function check($container_definition) { return isset($container_definition['services']['drush.service.consolecommands']) && isset($container_definition['services']['drush.service.consolidationcommands']); } } boot(), when Drupal's dependency injection container is being * compiled. Since we cannot use the container at this point (since its * initialization is not yet complete), we instead alter the definition of * a storage class in the container to add more setter injection method * calls to 'addCommandReference'. * * Later, after the container has been completely initialized, we can * fetch the storage class from the DI container (perhaps also via * injection from a reference in the container). At that point, we can * request the list of Console commands that were added via the * (delayed) call(s) to addCommandReference. * * Documentation: * * http://symfony.com/doc/2.7/components/dependency_injection/tags.html#create-a-compilerpass */ class FindCommandsCompilerPass implements CompilerPassInterface { protected $storageClassId; protected $tagId; public function __construct($storageClassId, $tagId) { $this->storageClassId = $storageClassId; $this->tagId = $tagId; } public function process(ContainerBuilder $container) { drush_log(dt("process !storage !tag", ['!storage' => $this->storageClassId, '!tag' => $this->tagId]), LogLevel::DEBUG); // We expect that our called registered the storage // class under the storage class id before adding this // compiler pass, but we will test this presumption to be sure. if (!$container->has($this->storageClassId)) { drush_log(dt("storage class not registered"), LogLevel::DEBUG); return; } $definition = $container->findDefinition( $this->storageClassId ); $taggedServices = $container->findTaggedServiceIds( $this->tagId ); foreach ($taggedServices as $id => $tags) { drush_log(dt("found tagged service !id", ['!id' => $id]), LogLevel::DEBUG); $definition->addMethodCall( 'addCommandReference', array(new Reference($id)) ); } } } parser = $parser; $this->logger = $logger; } /** * {@inheritdoc} */ public function log($level, $message, array $context = array()) { // Translate the RFC logging levels into their Drush counterparts, more or // less. // @todo ALERT, CRITICAL and EMERGENCY are considered show-stopping errors, // and they should cause Drush to exit or panic. Not sure how to handle this, // though. switch ($level) { case RfcLogLevel::ALERT: case RfcLogLevel::CRITICAL: case RfcLogLevel::EMERGENCY: case RfcLogLevel::ERROR: $error_type = LogLevel::ERROR; break; case RfcLogLevel::WARNING: $error_type = LogLevel::WARNING; break; // TODO: RfcLogLevel::DEBUG should be 'debug' rather than 'notice'? case RfcLogLevel::DEBUG: case RfcLogLevel::INFO: case RfcLogLevel::NOTICE: $error_type = LogLevel::NOTICE; break; // TODO: Unknown log levels that are not defined // in Psr\Log\LogLevel or Drush\Log\LogLevel SHOULD NOT be used. See // https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md // We should convert these to 'notice'. default: $error_type = $level; break; } // Populate the message placeholders and then replace them in the message. $message_placeholders = $this->parser->parseMessagePlaceholders($message, $context); // Filter out any placeholders that can not be cast to strings. $message_placeholders = array_filter($message_placeholders, function ($element) { return is_scalar($element) || is_callable([$element, '__toString']); }); $message = empty($message_placeholders) ? $message : strtr($message, $message_placeholders); $this->logger->log($error_type, $message, $context); } } $parent[$key]); } if (!isset($parent[$key]) || !is_array($parent[$key])) { $parent[$key] = array(); } $parent = &$parent[$key]; } // Handle PHP constants. if (defined($value)) { $value = constant($value); } // Insert actual value. if ($last == '') { $last = count($parent); } if (isset($merge_item) && isset($parent[$last]) && is_array($parent[$last])) { $parent[$last][$merge_item] = $value; } else { $parent[$last] = $value; } } return $info; } } } $item) { $array[BaseCaster::PREFIX_PROTECTED . $property] = $item; } } return $array; } /** * Casts \Drupal\Core\Field\FieldItemListInterface classes. */ public static function castFieldItemList($list_item, $array, $stub, $isNested) { if (!$isNested) { foreach ($list_item as $delta => $item) { $array[BaseCaster::PREFIX_VIRTUAL . $delta] = $item; } } return $array; } /** * Casts \Drupal\Core\Field\FieldItemInterface classes. */ public static function castFieldItem($item, $array, $stub, $isNested) { if (!$isNested) { $array[BaseCaster::PREFIX_VIRTUAL . 'value'] = $item->getValue(); } return $array; } /** * Casts \Drupal\Core\Config\Entity\ConfigEntityInterface classes. */ public static function castConfigEntity($entity, $array, $stub, $isNested) { if (!$isNested) { foreach ($entity->toArray() as $property => $value) { $array[BaseCaster::PREFIX_PROTECTED . $property] = $value; } } return $array; } /** * Casts \Drupal\Core\Config\ConfigBase classes. */ public static function castConfig($config, $array, $stub, $isNested) { if (!$isNested) { foreach ($config->get() as $property => $value) { $array[BaseCaster::PREFIX_VIRTUAL . $property] = $value; } } return $array; } /** * Casts \Drupal\Component\DependencyInjection\Container classes. */ public static function castContainer($container, $array, $stub, $isNested) { if (!$isNested) { $service_ids = $container->getServiceIds(); sort($service_ids); foreach ($service_ids as $service_id) { $service = $container->get($service_id); $array[BaseCaster::PREFIX_VIRTUAL . $service_id] = is_object($service) ? get_class($service) : $service; } } return $array; } } config = $config; parent::__construct(); } /** * Get Category of this command. */ public function getCategory() { return $this->category; } /** * Sets the category title. * * @param string $category_title */ public function setCategory($category_title) { $this->category = $category_title; } /** * {@inheritdoc} */ protected function configure() { $this ->setName($this->config['command']) ->setAliases($this->buildAliasesFromConfig()) ->setDefinition($this->buildDefinitionFromConfig()) ->setDescription($this->config['description']) ->setHelp($this->buildHelpFromConfig()); } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $args = $input->getArguments(); $first = array_shift($args); // If the first argument is an alias, assign the next argument as the // command. if (strpos($first, '@') === 0) { $alias = $first; $command = array_shift($args); } // Otherwise, default the alias to '@self' and use the first argument as the // command. else { $alias = '@self'; $command = $first; } $options = $input->getOptions(); // Force the 'backend' option to TRUE. $options['backend'] = TRUE; $return = drush_invoke_process($alias, $command, array_values($args), $options, ['interactive' => TRUE]); if ($return['error_status'] > 0) { foreach ($return['error_log'] as $error_type => $errors) { $output->write($errors); } // Add a newline after so the shell returns on a new line. $output->writeln(''); } else { $output->page(drush_backend_get_result()); } } /** * Extract Drush command aliases from config array. * * @return array * The command aliases. */ protected function buildAliasesFromConfig() { return !empty($this->config['aliases']) ? $this->config['aliases'] : []; } /** * Build a command definition from Drush command configuration array. * * Currently, adds all non-hidden arguments and options, and makes a decent * effort to guess whether an option accepts a value or not. It isn't always * right :P * * @return array * the command definition. */ protected function buildDefinitionFromConfig() { $definitions = []; if (isset($this->config['arguments']) && !empty($this->config['arguments'])) { $required_args = $this->config['required-arguments']; if ($required_args === FALSE) { $required_args = 0; } elseif ($required_args === TRUE) { $required_args = count($this->config['arguments']); } foreach ($this->config['arguments'] as $name => $argument) { if (!is_array($argument)) { $argument = ['description' => $argument]; } if (!empty($argument['hidden'])) { continue; } $input_type = ($required_args-- > 0) ? InputArgument::REQUIRED : InputArgument::OPTIONAL; $definitions[] = new InputArgument($name, $input_type, $argument['description'], NULL); } } // First create all global options. $options = $this->config['options'] + drush_get_global_options(); // Add command specific options. $definitions = array_merge($definitions, $this->createInputOptionsFromConfig($options)); return $definitions; } /** * Creates input definitions from command options. * * @param array $options_config * * @return \Symfony\Component\Console\Input\InputInterface[] */ protected function createInputOptionsFromConfig(array $options_config) { $definitions = []; foreach ($options_config as $name => $option) { // Some commands will conflict. if (in_array($name, ['help', 'command'])) { continue; } if (!is_array($option)) { $option = ['description' => $option]; } if (!empty($option['hidden'])) { continue; } // @todo: Figure out if there's a way to detect InputOption::VALUE_NONE // (i.e. flags) via the config array. if (isset($option['value']) && $option['value'] === 'required') { $input_type = InputOption::VALUE_REQUIRED; } else { $input_type = InputOption::VALUE_OPTIONAL; } $definitions[] = new InputOption($name, !empty($option['short-form']) ? $option['short-form'] : '', $input_type, $option['description']); } return $definitions; } /** * Build a command help from the Drush configuration array. * * Currently it's a word-wrapped description, plus any examples provided. * * @return string * The help string. */ protected function buildHelpFromConfig() { $help = wordwrap($this->config['description']); $examples = []; foreach ($this->config['examples'] as $ex => $def) { // Skip empty examples and things with obvious pipes... if (($ex === '') || (strpos($ex, '|') !== FALSE)) { continue; } $ex = preg_replace('/^drush\s+/', '', $ex); $examples[$ex] = $def; } if (!empty($examples)) { $help .= "\n\ne.g."; foreach ($examples as $ex => $def) { $help .= sprintf("\n// %s\n", wordwrap(OutputFormatter::escape($def), 75, "\n// ")); $help .= sprintf(">>> %s\n", OutputFormatter::escape($ex)); } } return $help; } } setName('help') ->setAliases(['?']) ->setDefinition([ new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', NULL), ]) ->setDescription('Show a list of commands. Type `help [foo]` for information about [foo].'); } /** * Helper for setting a subcommand to retrieve help for. * * @param \Symfony\Component\Console\Command\Command $command */ public function setCommand(Command $command) { $this->command = $command; } /** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { if ($this->command !== NULL) { // Help for an individual command. $output->page($this->command->asText()); $this->command = NULL; } elseif ($name = $input->getArgument('command_name')) { // Help for an individual command. $output->page($this->getApplication()->get($name)->asText()); } else { $categories = []; // List available commands. $commands = $this->getApplication()->all(); // Find the alignment width. $width = 0; foreach ($commands as $command) { $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; } $width += 2; foreach ($commands as $name => $command) { if ($name !== $command->getName()) { continue; } if ($command->getAliases()) { $aliases = sprintf(' Aliases: %s', implode(', ', $command->getAliases())); } else { $aliases = ''; } if ($command instanceof DrushCommand) { $category = (string) $command->getCategory(); } else { $category = static::PSYSH_CATEGORY; } if (!isset($categories[$category])) { $categories[$category] = []; } $categories[$category][] = sprintf(" %-${width}s %s%s", $name, $command->getDescription(), $aliases); } $messages = []; foreach ($categories as $name => $category) { $messages[] = ''; $messages[] = sprintf('%s', OutputFormatter::escape($name)); foreach ($category as $message) { $messages[] = $message; } } $output->page($messages); } } } getCommandFromInput($input)) { return $this->get($name); } } /** * Check whether a command is set for the current input string. * * @param string $input * * @return bool True if the shell has a command for the given input. */ protected function hasCommand($input) { if ($name = $this->getCommandFromInput($input)) { return $this->has($name); } return false; } /** * Get the command from the current input, takes aliases into account. * * @param string $input * The raw input * * @return string|NULL * The current command. */ protected function getCommandFromInput($input) { // Remove the alias from the start of the string before parsing and // returning the command. Essentially, when choosing a command, we're // ignoring the site alias. $input = preg_replace('|^\@[^\s]+|', '', $input); $input = new StringInput($input); return $input->getFirstArgument(); } } $queue) { static::$queues[$name]['worker callback'] = $queue['cron']['callback']; if (isset($queue['cron']['time'])) { static::$queues[$name]['time'] = $queue['cron']['time']; } } } return static::$queues; } /** * {@inheritdoc} * * @return \DrupalQueueInterface */ public function getQueue($name) { return DrupalQueue::get($name); } /** * {@inheritdoc} */ public function run($name, $time_limit = 0) { $info = $this->getInfo($name); $function = $info['worker callback']; $end = time() + $time_limit; $queue = $this->getQueue($name); $count = 0; while ((!$time_limit || time() < $end) && ($item = $queue->claimItem())) { try { drush_log(dt('Processing item @id from @name queue.', array('@name' => $name, 'id' => $item->item_id)), LogLevel::INFO); $function($item->data); $queue->deleteItem($item); $count++; } catch (\Exception $e) { // In case of exception log it and leave the item in the queue // to be processed again later. drush_set_error('DRUSH_QUEUE_EXCEPTION', $e->getMessage()); } } return $count; } } workerManager = $manager ?: \Drupal::service('plugin.manager.queue_worker'); } /** * {@inheritdoc} */ public function getQueues() { if (!isset(static::$queues)) { static::$queues = array(); foreach ($this->workerManager->getDefinitions() as $name => $info) { static::$queues[$name] = $info; } } return static::$queues; } /** * {@inheritdoc} * * @return \Drupal\Core\Queue\QueueInterface */ public function getQueue($name) { return \Drupal::queue($name); } /** * {@inheritdoc} */ public function run($name, $time_limit = 0) { $worker = $this->workerManager->createInstance($name); $end = time() + $time_limit; $queue = $this->getQueue($name); $count = 0; while ((!$time_limit || time() < $end) && ($item = $queue->claimItem())) { try { drush_log(dt('Processing item @id from @name queue.', array('@name' => $name, '@id' => $item->item_id)), LogLevel::INFO); $worker->processItem($item->data); $queue->deleteItem($item); $count++; } catch (RequeueException $e) { // The worker requested the task to be immediately requeued. $queue->releaseItem($item); } catch (SuspendQueueException $e) { // If the worker indicates there is a problem with the whole queue, // release the item and skip to the next queue. $queue->releaseItem($item); drush_set_error('DRUSH_SUSPEND_QUEUE_EXCEPTION', $e->getMessage()); } catch (\Exception $e) { // In case of any other kind of exception, log it and leave the item // in the queue to be processed again later. drush_set_error('DRUSH_QUEUE_EXCEPTION', $e->getMessage()); } } return $count; } } getQueues()) as $name) { $q = $this->getQueue($name); $result[$name] = array( 'queue' => $name, 'items' => $q->numberOfItems(), 'class' => get_class($q), ); } return $result; } /** * {@inheritdoc} */ public function getInfo($name) { $queues = $this->getQueues(); if (!isset($queues[$name])) { throw new QueueException(dt('Could not find the !name queue.', array('!name' => $name))); } return $queues[$name]; } } perms)) { $perms = db_result(db_query("SELECT perm FROM {permission} pm LEFT JOIN {role} r ON r.rid = pm.rid WHERE r.rid = '%d'", $this->rid)); $role_perms = explode(", ", $perms); $this->perms = array_filter($role_perms); } return $this->perms; } public function getModulePerms($module) { return module_invoke($module, 'perm'); } public function role_create($role_machine_name, $role_human_readable_name = '') { $this->_admin_user_role_op($role_machine_name, t('Add role')); return TRUE; } public function delete() { $this->_admin_user_role_op($this->rid, t('Delete role')); } function _admin_user_role_op($role_machine_name, $op) { // c.f. http://drupal.org/node/283261 require_once(drupal_get_path('module', 'user') . "/user.admin.inc"); $form_id = "user_admin_new_role"; $form_values = array(); $form_values["name"] = $role_machine_name; $form_values["op"] = $op; $form_state = array(); $form_state["values"] = $form_values; drupal_execute($form_id, $form_state); } public function grant_permissions($perms_to_add) { $perms = $this->getPerms(); $this->perms = array_unique(array_merge($this->perms, $perms_to_add)); $this->updatePerms(); } public function revoke_permissions($perms_to_remove) { $perms = $this->getPerms(); $this->perms = array_diff($this->perms, $perms_to_remove); $this->updatePerms(); } function updatePerms() { $new_perms = implode(", ", $this->perms); drush_op('db_query', "UPDATE {permission} SET perm = '%s' WHERE rid= %d", $new_perms, $this->rid); } } rid => $this->name)); return array_keys($perms[$this->rid]); } public function getModulePerms($module) { $perms = module_invoke($module, 'permission'); return $perms ? array_keys($perms) : array(); } public function role_create($role_machine_name, $role_human_readable_name = '') { return user_role_save((object)array('name' => $role_machine_name)); } public function delete() { user_role_delete($this->rid); } public function grant_permissions($perms) { return drush_op('user_role_grant_permissions', $this->rid, $perms); } public function revoke_permissions($perms) { return drush_op('user_role_revoke_permissions', $this->rid, $perms); } } $role_machine_name, 'label' => $role_human_readable_name, ), 'user_role'); $role->save(); return $role; } public function getPerms() { $role = entity_load('user_role', $this->rid); $perms = $role->getPermissions(); // $perms = user_role_permissions(array($this->rid => $this->name)); return $perms; } public function getAllModulePerms() { $perms = \Drupal::service('user.permissions')->getPermissions(); return array_keys($perms); } public function getModulePerms($module) { $module_perms = array(); $perms = \Drupal::service('user.permissions')->getPermissions(); foreach ($perms as $name => $perm) { if ($perm['provider'] == $module) { $module_perms[] = $name; } } return $module_perms; } public function delete() { $role = entity_load('user_role', $this->rid); $role->delete(); } public function grant_permissions($perms) { return drush_op('user_role_grant_permissions', $this->rid, $perms); } public function revoke_permissions($perms) { return drush_op('user_role_revoke_permissions', $this->rid, $perms); } } name pairs. */ public $roles; /** * This constructor will allow the role to be selected either * via the role id or via the role name. */ public function __construct($rid = DRUPAL_ANONYMOUS_RID) { $this->roles = user_roles(); if (!is_numeric($rid)) { $role_name = $rid; if (in_array($role_name, $this->roles)) { $rid = array_search($role_name, $this->roles); } } if (isset($this->roles[$rid])) { $this->rid = $rid; // In D8+ Role is an object. $this->name = is_object($this->roles[$rid]) ? $this->roles[$rid]->label() : $this->roles[$rid]; } else { throw new RoleException(dt('Could not find the role: !role', array('!role' => $rid))); } } /* * Get all perms for a given Role. */ public function getPerms() { return array(); } /* * Get all perms for a given module. */ public function getModulePerms($module) { return array(); } /* * Get all permissions site-wide. */ public function getAllModulePerms() { $permissions = array(); drush_include_engine('drupal', 'environment'); $module_list = drush_module_list(); ksort($module_list); foreach ($module_list as $module) { if ($perms = $this->getModulePerms($module)) { $permissions = array_merge($permissions, $perms); } } return $permissions; } public function role_create($role_machine_name, $role_human_readable_name = '') { } public function delete() { } public function add($perm) { $perms = $this->getPerms(); if (!in_array($perm, $perms)) { $this->grant_permissions(array($perm)); return TRUE; } else { drush_log(dt('"!role" already has the permission "!perm"', array( '!perm' => $perm, '!role' => $this->name, )), 'ok'); return FALSE; } } public function remove($perm) { $perms = $this->getPerms(); if (in_array($perm, $perms)) { $this->revoke_permissions(array($perm)); return TRUE; } else { drush_log(dt('"!role" does not have the permission "!perm"', array( '!perm' => $perm, '!role' => $this->name, )), 'ok'); return FALSE; } } public function grant_permissions($perms) { } public function revoke_permissions($perms) { } } $type)), LogLevel::BOOTSTRAP); return FALSE; } } else { drush_log(dt('!type database type is unsupported.', array('!type' => $type)), LogLevel::BOOTSTRAP); return FALSE; } return TRUE; } } db_spec = $db_spec; } /* * Get the current $db_spec. */ public function db_spec() { return $this->db_spec; } /** * The unix command used to connect to the database. * @return string */ public function command() {} /** * A string for connecting to a database. * * @param bool $hide_password * If TRUE, DBMS should try to hide password from process list. * On mysql, that means using --defaults-extra-file to supply the user+password. * * @return string */ public function connect($hide_password = TRUE) { return trim($this->command() . ' ' . $this->creds($hide_password) . ' ' . drush_get_option('extra', $this->query_extra)); } /* * Execute a SQL dump and return the path to the resulting dump file. * * @param string|bool @file * The path where the dump file should be stored. If TRUE, generate a path * based on usual backup directory and current date. */ public function dump($file = '') { $file_suffix = ''; $table_selection = $this->get_expanded_table_selection(); $file = $this->dumpFile($file); $cmd = $this->dumpCmd($table_selection); // Gzip the output from dump command(s) if requested. if (drush_get_option('gzip')) { $cmd .= ' | gzip -f'; $file_suffix .= '.gz'; } if ($file) { $file .= $file_suffix; $cmd .= ' > ' . drush_escapeshellarg($file); } // Avoid the php memory of the $output array in drush_shell_exec(). if (!$return = drush_op_system($cmd)) { if ($file) { drush_log(dt('Database dump saved to !path', array('!path' => $file)), LogLevel::SUCCESS); drush_backend_set_result($file); } } else { return drush_set_error('DRUSH_SQL_DUMP_FAIL', 'Database dump failed'); } } /* * Build bash for dumping a database. * * @param array $table_selection * Supported keys: 'skip', 'structure', 'tables'. * @return string * One or more mysqldump/pg_dump/sqlite3/etc statements that are ready for executing. * If multiple statements are needed, enclose in parenthesis. */ public function dumpCmd($table_selection) {} /* * Generate a path to an output file for a SQL dump when needed. * * @param string|bool @file * If TRUE, generate a path based on usual backup directory and current date. * Otherwise, just return the path that was provided. */ public function dumpFile($file) { $database = $this->db_spec['database']; // $file is passed in to us usually via --result-file. If the user // has set $options['result-file'] = TRUE, then we // will generate an SQL dump file in the same backup // directory that pm-updatecode uses. if ($file) { if ($file === TRUE) { // User did not pass a specific value for --result-file. Make one. $backup = drush_include_engine('version_control', 'backup'); $backup_dir = $backup->prepare_backup_dir($database); if (empty($backup_dir)) { $backup_dir = drush_find_tmp(); } $file = Path::join($backup_dir, '@DATABASE_@DATE.sql'); } $file = str_replace(array('@DATABASE', '@DATE'), array($database, gmdate('Ymd_His')), $file); } return $file; } /** * Execute a SQL query. * * Note: This is an API function. Try to avoid using drush_get_option() and instead * pass params in. If you don't want to query results to print during --debug then * provide a $result_file whose value can be drush_bit_bucket(). * * @param string $query * The SQL to be executed. Should be NULL if $input_file is provided. * @param string $input_file * A path to a file containing the SQL to be executed. * @param string $result_file * A path to save query results to. Can be drush_bit_bucket() if desired. * * @return * TRUE on success, FALSE on failure */ public function query($query, $input_file = NULL, $result_file = '') { $input_file_original = $input_file; if ($input_file && drush_file_is_tarball($input_file)) { if (drush_shell_exec('gzip -d %s', $input_file)) { $input_file = trim($input_file, '.gz'); } else { return drush_set_error(dt('Failed to decompress input file.')); } } // Save $query to a tmp file if needed. We will redirect it in. if (!$input_file) { $query = $this->query_prefix($query); $query = $this->query_format($query); $input_file = drush_save_data_to_temp_file($query); } $parts = array( $this->command(), $this->creds(), $this->silent(), // This removes column header and various helpful things in mysql. drush_get_option('extra', $this->query_extra), $this->query_file, drush_escapeshellarg($input_file), ); $exec = implode(' ', $parts); if ($result_file) { $exec .= ' > '. drush_escapeshellarg($result_file); } // In --verbose mode, drush_shell_exec() will show the call to mysql/psql/sqlite, // but the sql query itself is stored in a temp file and not displayed. // We show the query when --debug is used and this function created the temp file. if ((drush_get_context('DRUSH_DEBUG') || drush_get_context('DRUSH_SIMULATE')) && empty($input_file_original)) { drush_log('sql-query: ' . $query, LogLevel::NOTICE); } $success = drush_shell_exec($exec); if ($success && drush_get_option('file-delete')) { drush_op('drush_delete_dir', $input_file); } return $success; } /* * A string to add to the command when queries should not print their results. */ public function silent() {} public function query_prefix($query) { // Inject table prefixes as needed. if (drush_has_boostrapped(DRUSH_BOOTSTRAP_DRUPAL_DATABASE)) { // Enable prefix processing which can be dangerous so off by default. See http://drupal.org/node/1219850. if (drush_get_option('db-prefix')) { if (drush_drupal_major_version() >= 7) { $query = Database::getConnection()->prefixTables($query); } else { $query = db_prefix_tables($query); } } } return $query; } public function query_format($query) { return $query; } /** * Drop specified database. * * @param array $tables * An array of table names * @return boolean * True if successful, FALSE if failed. */ public function drop($tables) { $return = TRUE; if ($tables) { $sql = 'DROP TABLE '. implode(', ', $tables); $return = $this->query($sql); } return $return; } /** * Build a SQL string for dropping and creating a database. * * @param string dbname * The database name. * @param boolean $quoted * Quote the database name. Mysql uses backticks to quote which can cause problems * in a Windows shell. Set TRUE if the CREATE is not running on the bash command line. */ public function createdb_sql($dbname, $quoted = FALSE) {} /** * Create a new database. * * @param boolean $quoted * Quote the database name. Mysql uses backticks to quote which can cause problems * in a Windows shell. Set TRUE if the CREATE is not running on the bash command line. * @return boolean * True if successful, FALSE otherwise. */ public function createdb($quoted = FALSE) { $dbname = $this->db_spec['database']; $sql = $this->createdb_sql($dbname, $quoted); // Adjust connection to allow for superuser creds if provided. $this->su(); return $this->query($sql); } /** * Drop all tables (if DB exists) or CREATE target database. * * return boolean * TRUE or FALSE depending on success. */ public function drop_or_create() { if ($this->db_exists()) { return $this->drop($this->listTables()); } else { return $this->createdb(); } } /* * Determine if the specified DB already exists. * * @return bool */ public function db_exists() {} public function delete() {} /** * Build a fragment connection parameters. * * @param bool $hide_password * If TRUE, DBMS should try to hide password from process list. * On mysql, that means using --defaults-extra-file to supply the user+password. * @return string */ public function creds($hide_password = TRUE) {} /** * The active database driver. * @return string */ public function scheme() { return $this->db_spec['driver']; } /** * Get a list of all table names and expand input that may contain * wildcards (`*`) if necessary so that the array returned only contains valid * table names i.e. actual tables that exist, without a wildcard. * * @return array * An array of tables with each table name in the appropriate * element of the array. */ public function get_expanded_table_selection() { $table_selection = drush_sql_get_table_selection(); // Get the existing table names in the specified database. $db_tables = $this->listTables(); if (isset($table_selection['skip'])) { $table_selection['skip'] = _drush_sql_expand_and_filter_tables($table_selection['skip'], $db_tables); } if (isset($table_selection['structure'])) { $table_selection['structure'] = _drush_sql_expand_and_filter_tables($table_selection['structure'], $db_tables); } if (isset($table_selection['tables'])) { $table_selection['tables'] = _drush_sql_expand_and_filter_tables($table_selection['tables'], $db_tables); } return $table_selection; } /** * Extract the name of all existing tables in the given database. * * @return array * An array of table names which exist in the current database. */ public function listTables() {} /* * Helper method to turn associative array into options with values. * * @return string * A bash fragment. */ public function params_to_options($parameters) { // Turn each parameter into a valid parameter string. $parameter_strings = array(); foreach ($parameters as $key => $value) { // Only escape the values, not the keys or the rest of the string. $value = drush_escapeshellarg($value); $parameter_strings[] = "--$key=$value"; } // Join the parameters and return. return implode(' ', $parameter_strings); } /** * Adjust DB connection with superuser credentials if provided. * * The options 'db-su' and 'db-su-pw' will be retreived from the * specified site alias record, if it exists and contains those items. * If it does not, they will be fetched via drush_get_option. * * Note that in the context of sql-sync, the site alias record will * be taken from the target alias (e.g. `drush sql-sync @source @target`), * which will be overlayed with any options that begin with 'target-'; * therefore, the commandline options 'target-db-su' and 'target-db-su-pw' * may also affect the operation of this function. * * @return null */ public function su() { $create_db_target = $this->db_spec; $create_db_target['database'] = ''; $db_superuser = drush_get_option('db-su'); if (isset($db_superuser)) { $create_db_target['username'] = $db_superuser; } $db_su_pw = drush_get_option('db-su-pw'); // If --db-su-pw is not provided and --db-su is, default to empty password. // This way db cli command will take password from .my.cnf or .pgpass. if (!empty($db_su_pw)) { $create_db_target['password'] = $db_su_pw; } elseif (isset($db_superuser)) { unset($create_db_target['password']); } $this->db_spec = $create_db_target; } } db_spec['username']}" password="{$this->db_spec['password']}" EOT; $file = drush_save_data_to_temp_file($contents); $parameters['defaults-extra-file'] = $file; } else { // User is required. Drupal calls it 'username'. MySQL calls it 'user'. $parameters['user'] = $this->db_spec['username']; // EMPTY password is not the same as NO password, and is valid. if (isset($this->db_spec['password'])) { $parameters['password'] = $this->db_spec['password']; } } // Some drush commands (e.g. site-install) want to connect to the // server, but not the database. Connect to the built-in database. $parameters['database'] = empty($this->db_spec['database']) ? 'information_schema' : $this->db_spec['database']; // Default to unix socket if configured. if (!empty($this->db_spec['unix_socket'])) { $parameters['socket'] = $this->db_spec['unix_socket']; } // EMPTY host is not the same as NO host, and is valid (see unix_socket). elseif (isset($this->db_spec['host'])) { $parameters['host'] = $this->db_spec['host']; } if (!empty($this->db_spec['port'])) { $parameters['port'] = $this->db_spec['port']; } if (!empty($this->db_spec['pdo']['unix_socket'])) { $parameters['socket'] = $this->db_spec['pdo']['unix_socket']; } if (!empty($this->db_spec['pdo'][PDO::MYSQL_ATTR_SSL_CA])) { $parameters['ssl-ca'] = $this->db_spec['pdo'][PDO::MYSQL_ATTR_SSL_CA]; } if (!empty($this->db_spec['pdo'][PDO::MYSQL_ATTR_SSL_CAPATH])) { $parameters['ssl-capath'] = $this->db_spec['pdo'][PDO::MYSQL_ATTR_SSL_CAPATH]; } if (!empty($this->db_spec['pdo'][PDO::MYSQL_ATTR_SSL_CERT])) { $parameters['ssl-cert'] = $this->db_spec['pdo'][PDO::MYSQL_ATTR_SSL_CERT]; } if (!empty($this->db_spec['pdo'][PDO::MYSQL_ATTR_SSL_CIPHER])) { $parameters['ssl-cipher'] = $this->db_spec['pdo'][PDO::MYSQL_ATTR_SSL_CIPHER]; } if (!empty($this->db_spec['pdo'][PDO::MYSQL_ATTR_SSL_KEY])) { $parameters['ssl-key'] = $this->db_spec['pdo'][PDO::MYSQL_ATTR_SSL_KEY]; } return $this->params_to_options($parameters); } public function silent() { return '--silent'; } public function createdb_sql($dbname, $quoted = FALSE) { if ($quoted) { $dbname = '`' . $dbname . '`'; } $sql[] = sprintf('DROP DATABASE IF EXISTS %s;', $dbname); $sql[] = sprintf('CREATE DATABASE %s /*!40100 DEFAULT CHARACTER SET utf8 */;', $dbname); $db_superuser = drush_get_option('db-su'); if (isset($db_superuser)) { // - For a localhost database, create a localhost user. This is important for security. // localhost is special and only allows local Unix socket file connections. // - If the database is on a remote server, create a wilcard user with %. // We can't easily know what IP adderss or hostname would represent our server. $domain = ($this->db_spec['host'] == 'localhost') ? 'localhost' : '%'; $sql[] = sprintf('GRANT ALL PRIVILEGES ON %s.* TO \'%s\'@\'%s\'', $dbname, $this->db_spec['username'], $domain); $sql[] = sprintf("IDENTIFIED BY '%s';", $this->db_spec['password']); $sql[] = 'FLUSH PRIVILEGES;'; } return implode(' ', $sql); } public function db_exists() { $current = drush_get_context('DRUSH_SIMULATE'); drush_set_context('DRUSH_SIMULATE', FALSE); // Suppress output. We only care about return value. $return = $this->query("SELECT 1;", NULL, drush_bit_bucket()); drush_set_context('DRUSH_SIMULATE', $current); return $return; } public function listTables() { $current = drush_get_context('DRUSH_SIMULATE'); drush_set_context('DRUSH_SIMULATE', FALSE); $return = $this->query('SHOW TABLES;'); $tables = drush_shell_exec_output(); drush_set_context('DRUSH_SIMULATE', $current); return $tables; } public function dumpCmd($table_selection) { $parens = FALSE; $skip_tables = $table_selection['skip']; $structure_tables = $table_selection['structure']; $tables = $table_selection['tables']; $ignores = array(); $skip_tables = array_merge($structure_tables, $skip_tables); $data_only = drush_get_option('data-only'); // The ordered-dump option is only supported by MySQL for now. // @todo add documention once a hook for drush_get_option_help() is available. // @see drush_get_option_help() in drush.inc $ordered_dump = drush_get_option('ordered-dump'); $exec = 'mysqldump '; // mysqldump wants 'databasename' instead of 'database=databasename' for no good reason. $only_db_name = str_replace('--database=', ' ', $this->creds()); $exec .= $only_db_name; // We had --skip-add-locks here for a while to help people with insufficient permissions, // but removed it because it slows down the import a lot. See http://drupal.org/node/1283978 $extra = ' --no-autocommit --single-transaction --opt -Q'; if (isset($data_only)) { $extra .= ' --no-create-info'; } if (isset($ordered_dump)) { $extra .= ' --skip-extended-insert --order-by-primary'; } if ($option = drush_get_option('extra', $this->query_extra)) { $extra .= " $option"; } $exec .= $extra; if (!empty($tables)) { $exec .= ' ' . implode(' ', $tables); } else { // Append the ignore-table options. foreach ($skip_tables as $table) { $ignores[] = '--ignore-table=' . $this->db_spec['database'] . '.' . $table; $parens = TRUE; } $exec .= ' '. implode(' ', $ignores); // Run mysqldump again and append output if we need some structure only tables. if (!empty($structure_tables)) { $exec .= " && mysqldump " . $only_db_name . " --no-data $extra " . implode(' ', $structure_tables); $parens = TRUE; } } return $parens ? "($exec)" : $exec; } } db_spec['username'] . '/' . $this->db_spec['password'] . ($this->db_spec['host'] == 'USETNS' ? '@' . $this->db_spec['database'] : '@//' . $this->db_spec['host'] . ':' . ($db_spec['port'] ? $db_spec['port'] : '1521') . '/' . $this->db_spec['database']); } public function createdb_sql($dbname) { return drush_log("Unable to generate CREATE DATABASE sql for $dbname", LogLevel::ERROR); } // @todo $suffix = '.sql'; public function query_format($query) { // remove trailing semicolon from query if we have it $query = preg_replace('/\;$/', '', $query); // some sqlplus settings $settings[] = "set TRIM ON"; $settings[] = "set FEEDBACK OFF"; $settings[] = "set UNDERLINE OFF"; $settings[] = "set PAGES 0"; $settings[] = "set PAGESIZE 50000"; // are we doing a describe ? if (!preg_match('/^ *desc/i', $query)) { $settings[] = "set LINESIZE 32767"; } // are we doing a show tables ? if (preg_match('/^ *show tables/i', $query)) { $settings[] = "set HEADING OFF"; $query = "select object_name from user_objects where object_type='TABLE' order by object_name asc"; } // create settings string $sqlp_settings = implode("\n", $settings) . "\n"; // important for sqlplus to exit correctly return "${sqlp_settings}${query};\nexit;\n"; } public function listTables() { $return = $this->query("SELECT TABLE_NAME FROM USER_TABLES WHERE TABLE_NAME NOT IN ('BLOBS','LONG_IDENTIFIERS')"); $tables = drush_shell_exec_output(); if (!empty($tables)) { // Shift off the header of the column of data returned. array_shift($tables); return $tables; } } // @todo $file is no longer provided. We are supposed to return bash that can be piped to gzip. // Probably Oracle needs to override dump() entirely - http://stackoverflow.com/questions/2236615/oracle-can-imp-exp-go-to-stdin-stdout. public function dumpCmd($table_selection) { $create_db = drush_get_option('create-db'); $exec = 'exp ' . $this->creds(); // Change variable '$file' by reference in order to get drush_log() to report. if (!$file) { $file = $this->db_spec['username'] . '.dmp'; } $exec .= ' file=' . $file; if (!empty($tables)) { $exec .= ' tables="(' . implode(',', $tables) . ')"'; } $exec .= ' owner=' . $this->db_spec['username']; if ($option = drush_get_option('extra', $this->query_extra)) { $exec .= " $option"; } return array($exec, $file); } } db_spec['password'])) { $pgpass_parts = array( empty($this->db_spec['host']) ? 'localhost' : $this->db_spec['host'], empty($this->db_spec['port']) ? '5432' : $this->db_spec['port'], // Database '*', $this->db_spec['username'], $this->db_spec['password'] ); // Escape colon and backslash characters in entries. // @see http://www.postgresql.org/docs/9.1/static/libpq-pgpass.html array_walk($pgpass_parts, function (&$part) { // The order of the replacements is important so that backslashes are // not replaced twice. $part = str_replace(array('\\', ':'), array('\\\\', '\:'), $part); }); $pgpass_contents = implode(':', $pgpass_parts); $password_file = drush_save_data_to_temp_file($pgpass_contents); chmod($password_file, 0600); } return $password_file; } public function command() { $environment = ""; $pw_file = $this->password_file(); if (isset($pw_file)) { $environment = "PGPASSFILE={$pw_file} "; } return "{$environment}psql -q"; } /* * @param $hide_password * Not used in postgres. Use .pgpass file instead. See http://drupal.org/node/438828. */ public function creds($hide_password = TRUE) { // Some drush commands (e.g. site-install) want to connect to the // server, but not the database. Connect to the built-in database. $parameters['dbname'] = empty($this->db_spec['database']) ? 'template1' : $this->db_spec['database']; // Host and port are optional but have defaults. $parameters['host'] = empty($this->db_spec['host']) ? 'localhost' : $this->db_spec['host']; $parameters['port'] = empty($this->db_spec['port']) ? '5432' : $this->db_spec['port']; // Username is required. $parameters['username'] = $this->db_spec['username']; // Don't set the password. // @see http://drupal.org/node/438828 return $this->params_to_options($parameters); } public function createdb_sql($dbname, $quoted = FALSE) { if ($quoted) { $dbname = '`' . $dbname . '`'; } $sql[] = sprintf('drop database if exists %s;', $dbname); $sql[] = sprintf("create database %s ENCODING 'UTF8';", $dbname); return implode(' ', $sql); } public function db_exists() { $database = $this->db_spec['database']; // Get a new class instance that has no 'database'. $db_spec_no_db = $this->db_spec; unset($db_spec_no_db['database']); $sql_no_db = drush_sql_get_class($db_spec_no_db); $query = "SELECT 1 AS result FROM pg_database WHERE datname='$database'"; drush_shell_exec($sql_no_db->connect() . ' -t -c %s', $query); $output = drush_shell_exec_output(); return (bool)$output[0]; } public function query_format($query) { if (strtolower($query) == 'show tables;') { return PSQL_SHOW_TABLES; } return $query; } public function listTables() { $return = $this->query(PSQL_SHOW_TABLES); $tables = drush_shell_exec_output(); if (!empty($tables)) { return $tables; } return array(); } public function dumpCmd($table_selection) { $parens = FALSE; $skip_tables = $table_selection['skip']; $structure_tables = $table_selection['structure']; $tables = $table_selection['tables']; $ignores = array(); $skip_tables = array_merge($structure_tables, $skip_tables); $data_only = drush_get_option('data-only'); $create_db = drush_get_option('create-db'); $exec = 'pg_dump '; // Unlike psql, pg_dump does not take a '--dbname=' before the database name. $extra = str_replace('--dbname=', ' ', $this->creds()); if (isset($data_only)) { $extra .= ' --data-only'; } if ($option = drush_get_option('extra', $this->query_extra)) { $extra .= " $option"; } $exec .= $extra; $exec .= (!isset($create_db) && !isset($data_only) ? ' --clean' : ''); if (!empty($tables)) { foreach ($tables as $table) { $exec .= " --table=$table"; } } else { foreach ($skip_tables as $table) { $ignores[] = "--exclude-table=$table"; } $exec .= ' '. implode(' ', $ignores); // Run pg_dump again and append output if we need some structure only tables. if (!empty($structure_tables)) { $parens = TRUE; $schemaonlies = array(); foreach ($structure_tables as $table) { $schemaonlies[] = "--table=$table"; } $exec .= " && pg_dump --schema-only " . implode(' ', $schemaonlies) . $extra; $exec .= (!isset($create_db) && !isset($data_only) ? ' --clean' : ''); } } return $parens ? "($exec)" : $exec; } } db_spec['database']; } public function createdb_sql($dbname, $quoted = false) { return ''; } /** * Create a new database. * * @param boolean $quoted * Quote the database name. Mysql uses backticks to quote which can cause problems * in a Windows shell. Set TRUE if the CREATE is not running on the bash command line. */ public function createdb($quoted = FALSE) { $file = $this->db_spec['database']; if (file_exists($file)) { drush_log("SQLITE: Deleting existing database '$file'", LogLevel::DEBUG); drush_delete_dir($file, TRUE); } // Make sure sqlite can create file $path = dirname($file); drush_log("SQLITE: creating '$path' for creating '$file'", LogLevel::DEBUG); drush_mkdir($path); if (!file_exists($path)) { drush_log("SQLITE: Cannot create $path", LogLevel::ERROR); return FALSE; } else { return TRUE; } } public function db_exists() { return file_exists($this->db_spec['database']); } public function listTables() { $return = $this->query('.tables'); $tables_raw = drush_shell_exec_output(); // SQLite's '.tables' command always outputs the table names in a column // format, like this: // table_alpha table_charlie table_echo // table_bravo table_delta table_foxtrot // …and there doesn't seem to be a way to fix that. So we need to do some // clean-up. foreach ($tables_raw as $line) { preg_match_all('/[^\s]+/', $line, $matches); if (!empty($matches[0])) { foreach ($matches[0] as $match) { $tables[] = $match; } } } return $tables; } public function drop($tables) { $sql = ''; // SQLite only wants one table per DROP TABLE command (so we have to do // "DROP TABLE foo; DROP TABLE bar;" instead of "DROP TABLE foo, bar;"). foreach ($tables as $table) { $sql .= "DROP TABLE $table; "; } return $this->query($sql); } public function dumpCmd($table_selection) { // Dumping is usually not necessary in SQLite, since all database data // is stored in a single file which can be copied just // like any other file. But it still has a use in migration purposes and // building human-readable diffs and such, so let's do it anyway. $exec = $this->connect(); // SQLite's dump command doesn't support many of the features of its // Postgres or MySQL equivalents. We may be able to fake some in the // future, but for now, let's just support simple dumps. $exec .= ' ".dump"'; if ($option = drush_get_option('extra', $this->query_extra)) { $exec .= " $option"; } return $exec; } } db_spec['database']) ? 'master' : $this->db_spec['database']; // Host and port are optional but have defaults. $host = empty($this->db_spec['host']) ? '.\SQLEXPRESS' : $this->db_spec['host']; if ($this->db_spec['username'] == '') { return ' -S ' . $host . ' -d ' . $database; } else { return ' -S ' . $host . ' -d ' . $database . ' -U ' . $this->db_spec['username'] . ' -P ' . $this->db_spec['password']; } } public function db_exists() { // TODO: untested, but the gist is here. $database = $this->db_spec['database']; // Get a new class instance that has no 'database'. $db_spec_no_db = $this->db_spec; unset($db_spec_no_db['database']); $sql_no_db = drush_sql_get_class($db_spec_no_db); $query = "if db_id('$database') IS NOT NULL print 1"; drush_shell_exec($sql_no_db->connect() . ' -Q %s', $query); $output = drush_shell_exec_output(); return $output[0] == 1; } public function listTables() { $return = $this->query('SELECT TABLE_NAME FROM information_schema.tables'); $tables = drush_shell_exec_output(); if (!empty($tables)) { // Shift off the header of the column of data returned. array_shift($tables); return $tables; } } // @todo $file is no longer provided. We are supposed to return bash that can be piped to gzip. // Probably sqlsrv needs to override dump() entirely. public function dumpCmd($table_selection) { if (!$file) { $file = $this->db_spec['database'] . '_' . date('Ymd_His') . '.bak'; } $exec = "sqlcmd -U \"" . $this->db_spec['username'] . "\" -P \"" . $this->db_spec['password'] . "\" -S \"" . $this->db_spec['host'] . "\" -Q \"BACKUP DATABASE [" . $this->db_spec['database'] . "] TO DISK='" . $file . "'\""; if ($option = drush_get_option('extra', $this->query_extra)) { $exec .= " $option"; } return array($exec, $file); } } =7 requires PDO and Drush requires php 5.4+ which ships with PDO // but it may be compiled with --disable-pdo. if (!class_exists('\PDO')) { drush_log(dt('PDO support is required.'), LogLevel::BOOTSTRAP); return FALSE; } return TRUE; } } xpath('/error')) { $error = (string)$error[0]; if (strpos($error, 'No release history available for') === 0) { $project_status = 'unsupported'; } elseif (strpos($error, 'No release history was found for the requested project') === 0) { $project_status = 'unknown'; } // Any other error we are not aware of. else { $project_status = 'unknown'; } } // The xml has a project, but still it can have errors. else { $this->parsed = self::parseXml($xml); if (empty($this->parsed['releases'])) { $error = dt('No available releases found for the requested project (!name).', array('!name' => $this->parsed['short_name'])); $project_status = 'unknown'; } else { $error = FALSE; $project_status = $xml->xpath('/project/project_status'); $project_status = (string)$project_status[0]; } } $this->project_status = $project_status; $this->error = $error; if ($error) { drush_set_error('DRUSH_RELEASE_INFO_ERROR', $error); } } /** * Downloads release info xml from update service. * * @param array $request * A request array. * @param int $cache_duration * Cache lifetime. * * @return \Drush\UpdateService\Project */ public static function getInstance(array $request, $cache_duration = ReleaseInfo::CACHE_LIFETIME) { $url = self::buildFetchUrl($request); drush_log(dt('Downloading release history from !url', array('!url' => $url))); $path = drush_download_file($url, drush_tempnam($request['name']), $cache_duration); $xml = simplexml_load_file($path); if (!$xml) { $error = dt('Failed to get available update data from !url', array('!url' => $url)); return drush_set_error('DRUSH_RELEASE_INFO_ERROR', $error); } return new Project($xml); } /** * Returns URL to the updates service for the given request. * * @param array $request * A request array. * * @return string * URL to the updates service. * * @see \Drupal\update\UpdateFetcher::buildFetchUrl() */ public static function buildFetchUrl(array $request) { $status_url = isset($request['status url']) ? $request['status url'] : ReleaseInfo::DEFAULT_URL; return $status_url . '/' . $request['name'] . '/' . $request['drupal_version']; } /** * Parses update service xml. * * @param \SimpleXMLElement $xml * XML element from the updates service. * * @return array * Project update information. */ private static function parseXml(\SimpleXMLElement $xml) { $project_info = array(); // Extract general project info. $items = array('title', 'short_name', 'dc:creator', 'type', 'api_version', 'recommended_major', 'supported_majors', 'default_major', 'project_status', 'link', ); foreach ($items as $item) { if (array_key_exists($item, (array)$xml)) { $value = $xml->xpath($item); $project_info[$item] = (string)$value[0]; } } // Parse project type. $project_types = array( 'core' => 'project_core', 'profile' => 'project_distribution', 'module' => 'project_module', 'theme' => 'project_theme', 'theme engine' => 'project_theme_engine', 'translation' => 'project_translation', 'utility' => 'project_drupalorg', ); $type = $project_info['type']; // Probably unused but kept for possible legacy compat. $type = ($type == 'profile-legacy') ? 'profile' : $type; $project_info['project_type'] = array_search($type, $project_types); // Extract project terms. $project_info['terms'] = array(); if ($xml->terms) { foreach ($xml->terms->children() as $term) { $term_name = (string) $term->name; $term_value = (string) $term->value; if (!isset($project_info[$term_name])) { $project_info['terms'][$term_name] = array(); } $project_info['terms'][$term_name][] = $term_value; } } // Extract and parse releases info. // In addition to the info in the update service, here we calculate // release statuses as Recommended, Security, etc. $recommended_major = empty($project_info['recommended_major']) ? '' : $project_info['recommended_major']; $supported_majors = empty($project_info['supported_majors']) ? array() : array_flip(explode(',', $project_info['supported_majors'])); $items = array( 'name', 'date', 'status', 'type', 'version', 'tag', 'version_major', 'version_patch', 'version_extra', 'release_link', 'download_link', 'mdhash', 'filesize', ); $releases = array(); $releases_xml = @$xml->xpath("/project/releases/release[status='published']"); foreach ($releases_xml as $release) { $release_info = array(); $statuses = array(); // Extract general release info. foreach ($items as $item) { if (array_key_exists($item, $release)) { $value = $release->xpath($item); $release_info[$item] = (string)$value[0]; } } // Extract release terms. $release_info['terms'] = array(); if ($release->terms) { foreach ($release->terms->children() as $term) { $term_name = (string) $term->name; $term_value = (string) $term->value; if (!isset($release_info['terms'][$term_name])) { $release_info['terms'][$term_name] = array(); } $release_info['terms'][$term_name][] = $term_value; // Add "Security" for security updates, and nothing // for the other kinds. if (strpos($term_value, "Security") !== FALSE) { $statuses[] = "Security"; } } } // Extract files. $release_info['files'] = array(); foreach ($release->files->children() as $file) { // Normalize keys to match the ones in the release info. $item = array( 'download_link' => (string) $file->url, 'date' => (string) $file->filedate, 'mdhash' => (string) $file->md5, 'filesize' => (string) $file->size, 'archive_type' => (string) $file->archive_type, ); if (!empty($file->variant)) { $item['variant'] = (string) $file->variant; } $release_info['files'][] = $item; } // Calculate statuses. if (array_key_exists($release_info['version_major'], $supported_majors)) { $statuses[] = "Supported"; unset($supported_majors[$release_info['version_major']]); } if ($release_info['version_major'] == $recommended_major) { if (!isset($latest_version)) { $latest_version = $release_info['version']; } // The first stable version (no 'version extra') in the recommended major // is the recommended release if (empty($release_info['version_extra']) && (!isset($recommended_version))) { $statuses[] = "Recommended"; $recommended_version = $release_info['version']; } } if (!empty($release_info['version_extra']) && ($release_info['version_extra'] == "dev")) { $statuses[] = "Development"; } $release_info['release_status'] = $statuses; $releases[$release_info['version']] = $release_info; } // If there's no "Recommended major version", we want to recommend // the most recent release. if (!$recommended_major) { $latest_version = key($releases); } // If there is no -stable- release in the recommended major, // then take the latest version in the recommended major to be // the recommended release. if (!isset($recommended_version) && isset($latest_version)) { $recommended_version = $latest_version; $releases[$recommended_version]['release_status'][] = "Recommended"; } $project_info['releases'] = $releases; if (isset($recommended_version)) { $project_info['recommended'] = $recommended_version; } return $project_info; } /** * Gets the project type. * * @return string * Type of the project. */ public function getType() { return $this->parsed['project_type']; } /** * Gets the project status in the update service. * * This is the project status in drupal.org: insecure, revoked, published etc. * * @return string */ public function getStatus() { return $this->project_status; } /** * Whether this object represents a project in the update service or an error. */ public function isValid() { return ($this->error === FALSE); } /** * Gets the parsed xml. * * @return array or FALSE if the xml has an error. */ public function getInfo() { return (!$this->error) ? $this->parsed : FALSE; } /** * Helper to pick the best release in a list of candidates. * * The best one is the first stable release if there are stable * releases; otherwise, it will be the first of the candidates. * * @param array $releases * Array of release arrays. * * @return array|bool */ public static function getBestRelease(array $releases) { if (empty($releases)) { return FALSE; } else { // If there are releases found, let's try first to fetch one with no // 'version_extra'. Otherwise, use all. $stable_releases = array(); foreach ($releases as $one_release) { if (!array_key_exists('version_extra', $one_release)) { $stable_releases[] = $one_release; } } if (!empty($stable_releases)) { $releases = $stable_releases; } } // First published release is just the first value in $releases. return reset($releases); } private function searchReleases($key, $value) { $releases = array(); foreach ($this->parsed['releases'] as $version => $release) { if ($release['status'] == 'published' && isset($release[$key]) && strcmp($release[$key], $value) == 0) { $releases[$version] = $release; } } return $releases; } /** * Returns the specific release that matches the request version. * * @param string $version * Version of the release to pick. * @return array|bool * The release or FALSE if no version specified or no release found. */ public function getSpecificRelease($version = NULL) { if (!empty($version)) { $matches = array(); // See if we only have a branch version. if (preg_match('/^\d+\.x-(\d+)$/', $version, $matches)) { $releases = $this->searchReleases('version_major', $matches[1]); } else { // In some cases, the request only says something like '7.x-3.x' but the // version strings include '-dev' on the end, so we need to append that // here for the xpath to match below. if (substr($version, -2) == '.x') { $version .= '-dev'; } $releases = $this->searchReleases('version', $version); } if (empty($releases)) { return FALSE; } return self::getBestRelease($releases); } return array(); } /** * Pick the first dev release from XML list. * * @return array|bool * The selected release xml object or FALSE. */ public function getDevRelease() { $releases = $this->searchReleases('version_extra', 'dev'); return self::getBestRelease($releases); } /** * Pick most appropriate release from XML list. * * @return array|bool * The selected release xml object or FALSE. */ public function getRecommendedOrSupportedRelease() { $majors = array(); $recommended_major = empty($this->parsed['recommended_major']) ? 0 : $this->parsed['recommended_major']; if ($recommended_major != 0) { $majors[] = $this->parsed['recommended_major']; } if (!empty($this->parsed['supported_majors'])) { $supported = explode(',', $this->parsed['supported_majors']); foreach ($supported as $v) { if ($v != $recommended_major) { $majors[] = $v; } } } $releases = array(); foreach ($majors as $major) { $releases = $this->searchReleases('version_major', $major); if (!empty($releases)) { break; } } return self::getBestRelease($releases); } /** * Comparison routine to order releases by date. * * @param array $a * Release to compare. * @param array $b * Release to compare. * * @return int * -1, 0 or 1 whether $a is greater, equal or lower than $b. */ private static function compareDates(array $a, array $b) { if ($a['date'] == $b['date']) { return ($a['version_major'] > $b['version_major']) ? -1 : 1; } if ($a['version_major'] == $b['version_major']) { return ($a['date'] > $b['date']) ? -1 : 1; } return ($a['version_major'] > $b['version_major']) ? -1 : 1; } /** * Comparison routine to order releases by version. * * @param array $a * Release to compare. * @param array $b * Release to compare. * * @return int * -1, 0 or 1 whether $a is greater, equal or lower than $b. */ private static function compareVersions(array $a, array $b) { $defaults = array( 'version_patch' => '', 'version_extra' => '', 'date' => 0, ); $a += $defaults; $b += $defaults; if ($a['version_major'] != $b['version_major']) { return ($a['version_major'] > $b['version_major']) ? -1 : 1; } else if ($a['version_patch'] != $b['version_patch']) { return ($a['version_patch'] > $b['version_patch']) ? -1 : 1; } else if ($a['version_extra'] != $b['version_extra']) { // Don't rely on version_extra alphabetical order. return ($a['date'] > $b['date']) ? -1 : 1; } return 0; } /** * Filter project releases by a criteria and returns a list. * * If no filter is provided, the first Recommended, Supported, Security * or Development release on each major version will be shown. * * @param string $filter * Valid values: * - 'all': Select all releases. * - 'dev': Select all development releases. * @param string $installed_version * Version string. If provided, Select all releases in the same * version_major branch until the provided one is found. * On any other branch, the default behaviour will be applied. * * @return array * List of releases matching the filter criteria. */ function filterReleases($filter = '', $installed_version = NULL) { $releases = $this->parsed['releases']; usort($releases, array($this, 'compareDates')); $installed_version = pm_parse_version($installed_version); // Iterate through and filter out the releases we're interested in. $options = array(); $limits_list = array(); foreach ($releases as $release) { $eligible = FALSE; // Mark as eligible if the filter criteria matches. if ($filter == 'all') { $eligible = TRUE; } elseif ($filter == 'dev') { if (!empty($release['version_extra']) && ($release['version_extra'] == 'dev')) { $eligible = TRUE; } } // The Drupal core version scheme (ex: 7.31) is different to // other projects (ex 7.x-3.2). We need to manage this special case. elseif (($this->getType() != 'core') && ($installed_version['version_major'] == $release['version_major'])) { // In case there's no filter, select all releases until the installed one. // Always show the dev release. if (isset($release['version_extra']) && ($release['version_extra'] == 'dev')) { $eligible = TRUE; } else { if (self::compareVersions($release, $installed_version) < 1) { $eligible = TRUE; } } } // Otherwise, pick only the first release in each status. // For example after we pick out the first security release, // we won't pick any other. We do this on a per-major-version basis, // though, so if a project has three major versions, then we will // pick out the first security release from each. else { foreach ($release['release_status'] as $one_status) { $test_key = $release['version_major'] . $one_status; if (empty($limits_list[$test_key])) { $limits_list[$test_key] = TRUE; $eligible = TRUE; } } } if ($eligible) { $options[$release['version']] = $release; } } // Add Installed status. if (!is_null($installed_version) && isset($options[$installed_version['version']])) { $options[$installed_version['version']]['release_status'][] = 'Installed'; } return $options; } /** * Prints release notes for given projects. * * @param string $version * Version of the release to get notes. * @param bool $print_status * Whether to print a informative note. * @param string $tmpfile * If provided, a file that contains contents to show before the * release notes. */ function getReleaseNotes($version = NULL, $print_status = TRUE, $tmpfile = NULL) { $project_name = $this->parsed['short_name']; if (!isset($tmpfile)) { $tmpfile = drush_tempnam('rln-' . $project_name . '.'); } // Select versions to show. $versions = array(); if (!is_null($version)) { $versions[] = $version; } else { // If requested project is installed, // show release notes for the installed version and all newer versions. if (isset($this->parsed['recommended'], $this->parsed['installed'])) { $releases = array_reverse($this->parsed['releases']); foreach($releases as $version => $release) { if ($release['date'] >= $this->parsed['releases'][$this->parsed['installed']]['date']) { $release += array('version_extra' => ''); $this->parsed['releases'][$this->parsed['installed']] += array('version_extra' => ''); if ($release['version_extra'] == 'dev' && $this->parsed['releases'][$this->parsed['installed']]['version_extra'] != 'dev') { continue; } $versions[] = $version; } } } else { // Project is not installed and user did not specify a version, // so show the release notes for the recommended version. $versions[] = $this->parsed['recommended']; } } foreach ($versions as $version) { if (!isset($this->parsed['releases'][$version]['release_link'])) { drush_log(dt("Project !project does not have release notes for version !version.", array('!project' => $project_name, '!version' => $version)), LogLevel::WARNING); continue; } // Download the release node page and get the html as xml to explore it. $release_link = $this->parsed['releases'][$version]['release_link']; $filename = drush_download_file($release_link, drush_tempnam($project_name)); @$dom = \DOMDocument::loadHTMLFile($filename); if ($dom) { drush_log(dt("Successfully parsed and loaded the HTML contained in the release notes' page for !project (!version) project.", array('!project' => $project_name, '!version' => $version)), LogLevel::NOTICE); } else { drush_log(dt("Error while requesting the release notes page for !project project.", array('!project' => $project_name)), LogLevel::ERROR); continue; } $xml = simplexml_import_dom($dom); // Extract last update time and the notes. $last_updated = $xml->xpath('//div[contains(@class,"views-field-changed")]'); $last_updated = $last_updated[0]->asXML(); $notes = $xml->xpath('//div[contains(@class,"field-name-body")]'); $notes = (!empty($notes)) ? $notes[0]->asXML() : dt("There're no release notes."); // Build the notes header. $header = array(); $header[] = '
    '; $header[] = dt("> RELEASE NOTES FOR '!name' PROJECT, VERSION !version:", array('!name' => strtoupper($project_name), '!version' => $version)); $header[] = dt("> !last_updated.", array('!last_updated' => trim(drush_html_to_text($last_updated)))); if ($print_status) { $header[] = '> ' . implode(', ', $this->parsed['releases'][$version]['release_status']); } $header[] = '
    '; // Finally add the release notes for the requested project to the tmpfile. $content = implode("\n", $header) . "\n" . $notes . "\n"; #TODO# accept $html as a method argument if (!drush_get_option('html', FALSE)) { $content = drush_html_to_text($content, array('br', 'p', 'ul', 'ol', 'li', 'hr')); } file_put_contents($tmpfile, $content, FILE_APPEND); } #TODO# don't print! Just return the filename drush_print_file($tmpfile); } } engine_type = $type; $this->engine = $engine; if (is_null($config)) { $config = array(); } $config += array( 'cache-duration' => drush_get_option('cache-duration-releasexml', self::CACHE_LIFETIME), ); $this->engine_config = $config; $this->cache = array(); } /** * Returns configured cache duration. */ public function getCacheDuration() { return $this->engine_config['cache-duration']; } /** * Returns a project's release info from the update service. * * @param array $request * A request array. * * @param bool $refresh * Whether to discard cached object. * * @return \Drush\UpdateService\Project */ public function get($request, $refresh = FALSE) { if ($refresh || !isset($this->cache[$request['name']])) { $project_release_info = Project::getInstance($request, $this->getCacheDuration()); if ($project_release_info && !$project_release_info->isValid()) { $project_release_info = FALSE; } $this->cache[$request['name']] = $project_release_info; } return $this->cache[$request['name']]; } /** * Delete cached update service file of a project. * * @param array $request * A request array. */ public function clearCached(array $request) { if (isset($this->cache[$request['name']])) { unset($this->cache[$request['name']]); } $url = Project::buildFetchUrl($request); $cache_file = drush_download_file_name($url); if (file_exists($cache_file)) { unlink($cache_file); } } /** * Select the most appropriate release for a project, based on a strategy. * * @param Array &$request * A request array. * The array will be expanded with the project type. * @param String $restrict_to * One of: * 'dev': Forces choosing a -dev release. * 'version': Forces choosing a point release. * '': No restriction. * Default is ''. * @param String $select * Strategy for selecting a release, should be one of: * - auto: Try to select the latest release, if none found allow the user * to choose. * - always: Force the user to choose a release. * - never: Try to select the latest release, if none found then fail. * - ignore: Ignore and return NULL. * If no supported release is found, allow to ask the user to choose one. * @param Boolean $all * In case $select = TRUE this indicates that all available releases will be * offered the user to choose. * * @return array * The selected release. */ public function selectReleaseBasedOnStrategy($request, $restrict_to = '', $select = 'never', $all = FALSE, $version = NULL) { if (!in_array($select, array('auto', 'never', 'always', 'ignore'))) { return drush_set_error('DRUSH_PM_UNKNOWN_SELECT_STRATEGY', dt("Error: select strategy must be one of: auto, never, always, ignore", array())); } $project_release_info = $this->get($request); if (!$project_release_info) { return FALSE; } if ($select != 'always') { if (isset($request['version'])) { $release = $project_release_info->getSpecificRelease($request['version']); if ($release === FALSE) { return drush_set_error('DRUSH_PM_COULD_NOT_FIND_VERSION', dt("Could not locate !project version !version.", array( '!project' => $request['name'], '!version' => $request['version'], ))); } } if ($restrict_to == 'dev') { // If you specified a specific release AND --dev, that is either // redundant (okay), or contradictory (error). if (!empty($release)) { if ($release['version_extra'] != 'dev') { return drush_set_error('DRUSH_PM_COULD_NOT_FIND_VERSION', dt("You requested both --dev and !project version !version, which is not a '-dev' release.", array( '!project' => $request['name'], '!version' => $request['version'], ))); } } else { $release = $project_release_info->getDevRelease(); if ($release === FALSE) { return drush_set_error('DRUSH_PM_NO_DEV_RELEASE', dt('There is no development release for project !project.', array('!project' => $request['name']))); } } } // If there was no specific release requested, try to identify the most appropriate release. if (empty($release)) { $release = $project_release_info->getRecommendedOrSupportedRelease(); } if ($release) { return $release; } else { $message = dt('There are no stable releases for project !project.', array('!project' => $request['name'])); if ($select == 'never') { return drush_set_error('DRUSH_PM_NO_STABLE_RELEASE', $message); } drush_log($message, LogLevel::WARNING); if ($select == 'ignore') { return NULL; } } } // At this point the only chance is to ask the user to choose a release. if ($restrict_to == 'dev') { $filter = 'dev'; } elseif ($all) { $filter = 'all'; } else { $filter = ''; } $releases = $project_release_info->filterReleases($filter, $version); // Special checking: Drupal 6 is EOL, so there are no stable // releases for ANY contrib project. In this case, we'll default // to the best release, unless the user specified --select. $version_major = drush_drupal_major_version(); if (($select != 'always') && ($version_major < 7)) { $bestRelease = Project::getBestRelease($releases); if (!empty($bestRelease)) { $message = dt('Drupal !major has reached EOL, so there are no stable releases for any contrib projects. Selected the best release, !project.', array('!major' => $version_major, '!project' => $bestRelease['name'])); drush_log($message, LogLevel::WARNING); return $bestRelease; } } $options = array(); foreach($releases as $release) { $options[$release['version']] = array($release['version'], '-', gmdate('Y-M-d', $release['date']), '-', implode(', ', $release['release_status'])); } $choice = drush_choice($options, dt('Choose one of the available releases for !project:', array('!project' => $request['name']))); if (!$choice) { return drush_user_abort(); } return $releases[$choice]; } /** * Check if a project is available in the update service. * * Optionally check for consistency by comparing given project type and * the type obtained from the update service. * * @param array $request * A request array. * @param string $type * Optional. If provided, will do a consistent check of the project type. * * @return boolean * True if the project exists and type matches. */ public function checkProject($request, $type = NULL) { $project_release_info = $this->get($request); if (!$project_release_info) { return FALSE; } if ($type) { if ($project_release_info->getType() != $type) { return FALSE; } } return TRUE; } } update_check_disabled = $conf['update_advanced_check_disabled']; $conf['update_advanced_check_disabled'] = $check_disabled; } } /** * {@inheritdoc} */ function afterGetStatus(&$update_info, $projects, $check_disabled) { // Restore Drupal settings. if (!is_null($check_disabled)) { global $conf; $conf['update_advanced_check_disabled'] = $this->update_check_disabled; unset($this->update_check_disabled); } // update_advanced.module sets a different project type // for disabled projects. Here we normalize it. if ($check_disabled) { foreach ($update_info as $key => $project) { if (in_array($project['project_type'], array('disabled-module', 'disabled-theme'))) { $update_info[$key]['project_type'] = substr($project['project_type'], strpos($project['project_type'], '-') + 1); } } } } /** * Obtains release info for all installed projects via update.module. * * @see update_get_available(). * @see update_manual_status(). */ protected function getAvailableReleases() { // We force a refresh if the cache is not available. if (!cache_get('update_available_releases', 'cache_update')) { $this->refresh(); } $available = update_get_available(TRUE); // Force to invalidate some update_status caches that are only cleared // when visiting update status report page. if (function_exists('_update_cache_clear')) { _update_cache_clear('update_project_data'); _update_cache_clear('update_project_projects'); } return $available; } } update_check_disabled = $conf['update_check_disabled']; $conf['update_check_disabled'] = $check_disabled; } } /** * {@inheritdoc} */ function afterGetStatus(&$update_info, $projects, $check_disabled) { // Restore Drupal settings. if (!is_null($check_disabled)) { global $conf; $conf['update_check_disabled'] = $this->update_check_disabled; unset($this->update_check_disabled); } // update.module sets a different project type // for disabled projects. Here we normalize it. if ($check_disabled) { foreach ($update_info as $key => $project) { if (in_array($project['project_type'], array('module-disabled', 'theme-disabled'))) { $update_info[$key]['project_type'] = substr($project['project_type'], 0, strpos($project['project_type'], '-')); } } } } /** * Obtains release info for all installed projects via update.module. * * @see update_get_available(). * @see update_manual_status(). */ protected function getAvailableReleases() { // Force to invalidate some caches that are only cleared // when visiting update status report page. This allow to detect changes in // .info files. _update_cache_clear('update_project_data'); _update_cache_clear('update_project_projects'); // From update_get_available(): Iterate all projects and create a fetch task // for those we have no information or is obsolete. $available = _update_get_cached_available_releases(); module_load_include('inc', 'update', 'update.compare'); $update_projects = update_get_projects(); foreach ($update_projects as $key => $project) { if (empty($available[$key])) { update_create_fetch_task($project); continue; } if ($project['info']['_info_file_ctime'] > $available[$key]['last_fetch']) { $available[$key]['fetch_status'] = UPDATE_FETCH_PENDING; } if (empty($available[$key]['releases'])) { $available[$key]['fetch_status'] = UPDATE_FETCH_PENDING; } if (!empty($available[$key]['fetch_status']) && $available[$key]['fetch_status'] == UPDATE_FETCH_PENDING) { update_create_fetch_task($project); } } // Set a batch to process all pending tasks. $batch = array( 'operations' => array( array('update_fetch_data_batch', array()), ), 'finished' => 'update_fetch_data_finished', 'file' => drupal_get_path('module', 'update') . '/update.fetch.inc', ); batch_set($batch); drush_backend_batch_process(); // Clear any error set by a failed update fetch task. This avoid rollbacks. drush_clear_error(); // Calculate update status data. $available = _update_get_cached_available_releases(); return $available; } } engine_type = $type; $this->engine = $engine; $this->engine_config = $config; } /** * {@inheritdoc} */ function lastCheck() { $last_check = \Drupal::state()->get('update.last_check') ?: 0; return $last_check; } /** * {@inheritdoc} */ function refresh() { update_refresh(); } /** * Perform adjustments before running get status. * * - Enforce check-disabled option on update module. */ function beforeGetStatus(&$projects, $check_disabled) { // If check-disabled option was provided, alter Drupal settings temporarily. // There's no other way to hook into this. if (!is_null($check_disabled)) { $config = \Drupal::config('update.settings'); $this->update_check_disabled = $config->get('check.disabled_extensions'); $config->set('check.disabled_extensions', (bool)$check_disabled); } } /** * Get update information for all installed projects. * * @return * Array of update status information. */ function getStatus($projects, $check_disabled) { $this->beforeGetStatus($projects, $check_disabled); $available = $this->getAvailableReleases(); $update_info = $this->calculateUpdateStatus($available, $projects); $this->afterGetStatus($update_info, $projects, $check_disabled); return $update_info; } /** * Perform adjustments after running get status. * * - Restore check-disabled setting in update module. * - Adjust project type for disabled projects. */ function afterGetStatus(&$update_info, $projects, $check_disabled) { // Restore Drupal settings. if (!is_null($check_disabled)) { \Drupal::config('update.settings')->set('check.disabled_extensions', $this->update_check_disabled); unset($this->update_check_disabled); } // update.module sets a different project type // for disabled projects. Here we normalize it. if ($check_disabled) { foreach ($update_info as $key => $project) { if (in_array($project['project_type'], array('module-disabled', 'theme-disabled'))) { $update_info[$key]['project_type'] = substr($project['project_type'], 0, strpos($project['project_type'], '-')); } } } } /** * Obtains release info for all installed projects via update.module. * * @see update_get_available(). * @see \Drupal\update\Controller\UpdateController::updateStatusManually() */ protected function getAvailableReleases() { // Force to invalidate some caches that are only cleared // when visiting update status report page. This allow to detect changes in // .info.yml files. \Drupal::keyValueExpirable('update')->deleteMultiple(array('update_project_projects', 'update_project_data')); // From update_get_available(): Iterate all projects and create a fetch task // for those we have no information or is obsolete. $available = \Drupal::keyValueExpirable('update_available_releases')->getAll(); $update_projects = \Drupal::service('update.manager')->getProjects(); foreach ($update_projects as $key => $project) { if (empty($available[$key])) { \Drupal::service('update.processor')->createFetchTask($project); continue; } if ($project['info']['_info_file_ctime'] > $available[$key]['last_fetch']) { $available[$key]['fetch_status'] = UPDATE_FETCH_PENDING; } if (empty($available[$key]['releases'])) { $available[$key]['fetch_status'] = UPDATE_FETCH_PENDING; } if (!empty($available[$key]['fetch_status']) && $available[$key]['fetch_status'] == UPDATE_FETCH_PENDING) { \Drupal::service('update.processor')->createFetchTask($project); } } // Set a batch to process all pending tasks. $batch = array( 'operations' => array( array(array(\Drupal::service('update.manager'), 'fetchDataBatch'), array()), ), 'finished' => 'update_fetch_data_finished', 'file' => drupal_get_path('module', 'update') . '/update.fetch.inc', ); batch_set($batch); drush_backend_batch_process(); // Clear any error set by a failed update fetch task. This avoid rollbacks. drush_clear_error(); return \Drupal::keyValueExpirable('update_available_releases')->getAll(); } /** * Calculates update status for all projects via update.module. */ protected function calculateUpdateStatus($available, $projects) { module_load_include('inc', 'update', 'update.compare'); $data = update_calculate_project_data($available); foreach ($data as $project_name => $project) { // Discard custom projects. if ($project['status'] == UPDATE_UNKNOWN) { unset($data[$project_name]); continue; } // Discard projects with unknown installation path. if ($project_name != 'drupal' && !isset($projects[$project_name]['path'])) { unset($data[$project_name]); continue; } // Add some info from the project to $data. $data[$project_name] += array( 'path' => isset($projects[$project_name]['path']) ? $projects[$project_name]['path'] : '', 'label' => $projects[$project_name]['label'], ); // Store all releases, not just the ones selected by update.module. // We use it to allow the user to update to a specific version. if (isset($available[$project_name]['releases'])) { $data[$project_name]['releases'] = $available[$project_name]['releases']; } } return $data; } } engine_type = $type; $this->engine = $engine; $this->engine_config = $config; } /** * {@inheritdoc} */ function lastCheck() { $older = 0; // Iterate all projects and get the time of the older release info. $projects = drush_get_projects(); foreach ($projects as $project_name => $project) { $request = pm_parse_request($project_name, NULL, $projects); $url = Project::buildFetchUrl($request); $cache_file = drush_download_file_name($url); if (file_exists($cache_file)) { $ctime = filectime($cache_file); $older = (!$older) ? $ctime : min($ctime, $older); } } return $older; } /** * {@inheritdoc} */ function refresh() { $release_info = drush_include_engine('release_info', 'updatexml'); // Clear all caches for the available projects. $projects = drush_get_projects(); foreach ($projects as $project_name => $project) { $request = pm_parse_request($project_name, NULL, $projects); $release_info->clearCached($request); } } /** * Get update information for all installed projects. * * @return * Array of update status information. */ function getStatus($projects, $check_disabled) { // Exclude disabled projects. if (!$check_disabled) { foreach ($projects as $project_name => $project) { if (!$project['status']) { unset($projects[$project_name]); } } } $available = $this->getAvailableReleases($projects); $update_info = $this->calculateUpdateStatus($available, $projects); return $update_info; } /** * Obtains release info for projects. */ private function getAvailableReleases($projects) { drush_log(dt('Checking available update data ...'), LogLevel::OK); $release_info = drush_include_engine('release_info', 'updatexml'); $available = array(); foreach ($projects as $project_name => $project) { // Discard projects with unknown installation path. if ($project_name != 'drupal' && !isset($project['path'])) { continue; } drush_log(dt('Checking available update data for !project.', array('!project' => $project['label'])), LogLevel::OK); $request = $project_name . (isset($project['core']) ? '-' . $project['core'] : ''); $request = pm_parse_request($request, NULL, $projects); $project_release_info = $release_info->get($request); if ($project_release_info) { $available[$project_name] = $project_release_info; } } // Clear any error set by a failed project. This avoid rollbacks. drush_clear_error(); return $available; } /** * Calculates update status for given projects. */ private function calculateUpdateStatus($available, $projects) { $update_info = array(); foreach ($available as $project_name => $project_release_info) { // Obtain project 'global' status. NULL status is ok (project published), // otherwise it signals something is bad with the project (revoked, etc). $project_status = $this->calculateProjectStatus($project_release_info); // Discard custom projects. if ($project_status == DRUSH_UPDATESTATUS_UNKNOWN) { continue; } // Prepare update info. $project = $projects[$project_name]; $is_core = ($project['type'] == 'core'); $version = pm_parse_version($project['version'], $is_core); // If project version ends with 'dev', this is a dev snapshot. $install_type = (substr($project['version'], -3, 3) == 'dev') ? 'dev' : 'official'; $project_update_info = array( 'name' => $project_name, 'label' => $project['label'], 'path' => isset($project['path']) ? $project['path'] : '', 'install_type' => $install_type, 'existing_version' => $project['version'], 'existing_major' => $version['version_major'], 'status' => $project_status, 'datestamp' => empty($project['datestamp']) ? NULL : $project['datestamp'], ); // If we don't have a project status yet, it means this is // a published project and we need to obtain its update status // and recommended release. if (is_null($project_status)) { $this->calculateProjectUpdateStatus($project_release_info, $project_update_info); } // We want to ship all release info data including all releases, // not just the ones selected by calculateProjectUpdateStatus(). // We use it to allow the user to update to a specific version. unset($project_update_info['releases']); $update_info[$project_name] = $project_update_info + $project_release_info->getInfo(); } return $update_info; } /** * Obtain the project status in the update service. * * This is not the update status of the installed version * but the project 'global' status (unpublished, revoked, etc). * * @see update_calculate_project_status(). */ private function calculateProjectStatus($project_release_info) { $project_status = NULL; // If connection to the update service went wrong, or the received xml // is malformed, we don't have a UpdateService::Project object. if (!$project_release_info) { $project_status = DRUSH_UPDATESTATUS_NOT_FETCHED; } else { switch ($project_release_info->getStatus()) { case 'insecure': $project_status = DRUSH_UPDATESTATUS_NOT_SECURE; break; case 'unpublished': case 'revoked': $project_status = DRUSH_UPDATESTATUS_REVOKED; break; case 'unsupported': $project_status = DRUSH_UPDATESTATUS_NOT_SUPPORTED; break; case 'unknown': $project_status = DRUSH_UPDATESTATUS_UNKNOWN; break; } } return $project_status; } /** * Obtain the update status of a project and the recommended release. * * This is a stripped down version of update_calculate_project_status(). * That function has the same logic in Drupal 6,7,8. * Note: in Drupal 6 this is part of update_calculate_project_data(). * * @see update_calculate_project_status(). */ private function calculateProjectUpdateStatus($project_release_info, &$project_data) { $available = $project_release_info->getInfo(); /** * Here starts the code adapted from update_calculate_project_status(). * Line 492 in Drupal 7. * * Changes are: * - Use DRUSH_UPDATESTATUS_* constants instead of DRUSH_UPDATESTATUS_* * - Remove error conditions we already handle * - Remove presentation code ('extra' and 'reason' keys in $project_data) * - Remove "also available" information. */ // Figure out the target major version. $existing_major = $project_data['existing_major']; $supported_majors = array(); if (isset($available['supported_majors'])) { $supported_majors = explode(',', $available['supported_majors']); } elseif (isset($available['default_major'])) { // Older release history XML file without supported or recommended. $supported_majors[] = $available['default_major']; } if (in_array($existing_major, $supported_majors)) { // Still supported, stay at the current major version. $target_major = $existing_major; } elseif (isset($available['recommended_major'])) { // Since 'recommended_major' is defined, we know this is the new XML // format. Therefore, we know the current release is unsupported since // its major version was not in the 'supported_majors' list. We should // find the best release from the recommended major version. $target_major = $available['recommended_major']; $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SUPPORTED; } elseif (isset($available['default_major'])) { // Older release history XML file without recommended, so recommend // the currently defined "default_major" version. $target_major = $available['default_major']; } else { // Malformed XML file? Stick with the current version. $target_major = $existing_major; } // Make sure we never tell the admin to downgrade. If we recommended an // earlier version than the one they're running, they'd face an // impossible data migration problem, since Drupal never supports a DB // downgrade path. In the unfortunate case that what they're running is // unsupported, and there's nothing newer for them to upgrade to, we // can't print out a "Recommended version", but just have to tell them // what they have is unsupported and let them figure it out. $target_major = max($existing_major, $target_major); $release_patch_changed = ''; $patch = ''; foreach ($available['releases'] as $version => $release) { // First, if this is the existing release, check a few conditions. if ($project_data['existing_version'] === $version) { if (isset($release['terms']['Release type']) && in_array('Insecure', $release['terms']['Release type'])) { $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SECURE; } elseif ($release['status'] == 'unpublished') { $project_data['status'] = DRUSH_UPDATESTATUS_REVOKED; } elseif (isset($release['terms']['Release type']) && in_array('Unsupported', $release['terms']['Release type'])) { $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SUPPORTED; } } // Otherwise, ignore unpublished, insecure, or unsupported releases. if ($release['status'] == 'unpublished' || (isset($release['terms']['Release type']) && (in_array('Insecure', $release['terms']['Release type']) || in_array('Unsupported', $release['terms']['Release type'])))) { continue; } // See if this is a higher major version than our target and discard it. // Note: at this point Drupal record it as an "Also available" release. if (isset($release['version_major']) && $release['version_major'] > $target_major) { continue; } // Look for the 'latest version' if we haven't found it yet. Latest is // defined as the most recent version for the target major version. if (!isset($project_data['latest_version']) && $release['version_major'] == $target_major) { $project_data['latest_version'] = $version; $project_data['releases'][$version] = $release; } // Look for the development snapshot release for this branch. if (!isset($project_data['dev_version']) && $release['version_major'] == $target_major && isset($release['version_extra']) && $release['version_extra'] == 'dev') { $project_data['dev_version'] = $version; $project_data['releases'][$version] = $release; } // Look for the 'recommended' version if we haven't found it yet (see // phpdoc at the top of this function for the definition). if (!isset($project_data['recommended']) && $release['version_major'] == $target_major && isset($release['version_patch'])) { if ($patch != $release['version_patch']) { $patch = $release['version_patch']; $release_patch_changed = $release; } if (empty($release['version_extra']) && $patch == $release['version_patch']) { $project_data['recommended'] = $release_patch_changed['version']; $project_data['releases'][$release_patch_changed['version']] = $release_patch_changed; } } // Stop searching once we hit the currently installed version. if ($project_data['existing_version'] === $version) { break; } // If we're running a dev snapshot and have a timestamp, stop // searching for security updates once we hit an official release // older than what we've got. Allow 100 seconds of leeway to handle // differences between the datestamp in the .info file and the // timestamp of the tarball itself (which are usually off by 1 or 2 // seconds) so that we don't flag that as a new release. if ($project_data['install_type'] == 'dev') { if (empty($project_data['datestamp'])) { // We don't have current timestamp info, so we can't know. continue; } elseif (isset($release['date']) && ($project_data['datestamp'] + 100 > $release['date'])) { // We're newer than this, so we can skip it. continue; } } // See if this release is a security update. if (isset($release['terms']['Release type']) && in_array('Security update', $release['terms']['Release type'])) { $project_data['security updates'][] = $release; } } // If we were unable to find a recommended version, then make the latest // version the recommended version if possible. if (!isset($project_data['recommended']) && isset($project_data['latest_version'])) { $project_data['recommended'] = $project_data['latest_version']; } // // Check to see if we need an update or not. // if (!empty($project_data['security updates'])) { // If we found security updates, that always trumps any other status. $project_data['status'] = DRUSH_UPDATESTATUS_NOT_SECURE; } if (isset($project_data['status'])) { // If we already know the status, we're done. return; } // If we don't know what to recommend, there's nothing we can report. // Bail out early. if (!isset($project_data['recommended'])) { $project_data['status'] = DRUSH_UPDATESTATUS_UNKNOWN; $project_data['reason'] = t('No available releases found'); return; } // If we're running a dev snapshot, compare the date of the dev snapshot // with the latest official version, and record the absolute latest in // 'latest_dev' so we can correctly decide if there's a newer release // than our current snapshot. if ($project_data['install_type'] == 'dev') { if (isset($project_data['dev_version']) && $available['releases'][$project_data['dev_version']]['date'] > $available['releases'][$project_data['latest_version']]['date']) { $project_data['latest_dev'] = $project_data['dev_version']; } else { $project_data['latest_dev'] = $project_data['latest_version']; } } // Figure out the status, based on what we've seen and the install type. switch ($project_data['install_type']) { case 'official': if ($project_data['existing_version'] === $project_data['recommended'] || $project_data['existing_version'] === $project_data['latest_version']) { $project_data['status'] = DRUSH_UPDATESTATUS_CURRENT; } else { $project_data['status'] = DRUSH_UPDATESTATUS_NOT_CURRENT; } break; case 'dev': $latest = $available['releases'][$project_data['latest_dev']]; if (empty($project_data['datestamp'])) { $project_data['status'] = DRUSH_UPDATESTATUS_NOT_CHECKED; } elseif (($project_data['datestamp'] + 100 > $latest['date'])) { $project_data['status'] = DRUSH_UPDATESTATUS_CURRENT; } else { $project_data['status'] = DRUSH_UPDATESTATUS_NOT_CURRENT; } break; default: $project_data['status'] = DRUSH_UPDATESTATUS_UNKNOWN; } } } $name)); } /** * {@inheritdoc} */ public function load_by_mail($mail) { return user_load(array('mail' => $mail)); } } save(); return new UserSingle8($account); } /** * Attempt to load a user account. * * @param int $uid * @return \Drupal\user\Entity\User; */ public function load_by_uid($uid) { return User::load($uid); } /** * {inheritdoc} */ public function getCurrentUserAsAccount() { return \Drupal::currentUser()->getAccount(); } /** * Set the current user in Drupal. * * @param \Drupal\Core\Session\AccountInterface $account */ public function setCurrentUser($account) { // Some parts of Drupal still rely on a global user object. // @todo remove once https://www.drupal.org/node/2163205 is in. global $user; $user = $account; \Drupal::currentUser()->setAccount($account); } } accounts = $this->getFromOptions() + $this->getFromParameters($inputs)) { return $this; } else { throw new UserListException('Unable to find a matching user.'); } } /** * Iterate over each account and call the specified method. * * @param $method * A method on a UserSingleBase object. * @param array $params * An array of params to pass to the method. * @return array * An associate array of values keyed by account ID. */ public function each($method, array $params = array()) { foreach ($this->accounts as $account) { $return[$account->id()] = call_user_func_array(array($account, $method), $params); } return $return; } /* * Check common options for specifying users. If valid, return the accounts. * * @return \Drush\User\UserSingleBase[] */ function getFromOptions() { $accounts = array(); $userversion = drush_user_get_class(); if ($mails = _convert_csv_to_array(drush_get_option('mail'))) { foreach ($mails as $mail) { if ($account = $userversion->load_by_mail($mail)) { $single = drush_usersingle_get_class($account); $accounts[$single->id()] = $single; } else { throw new UserListException('Unable to find a matching user for ' . $mail . '.'); } } } if ($names = _convert_csv_to_array(drush_get_option('name'))) { foreach ($names as $name) { if ($account = $userversion->load_by_name($name)) { $single = drush_usersingle_get_class($account); $accounts[$single->id()] = $single; } else { throw new UserListException('Unable to find a matching user for ' . $name . '.'); } } } if ($userids = _convert_csv_to_array(drush_get_option('uid'))) { foreach ($userids as $userid) { if (is_numeric($userid) && $account = $userversion->load_by_uid($userid)) { $single = drush_usersingle_get_class($account); $accounts[$single->id()] = $single; } else { throw new UserListException('Unable to find a matching user for ' . $userid . '.'); } } } return $accounts; } /** * Given a comma-separated list of inputs, return accounts * for users that match by uid,name or email address. * * @param string $inputs * A comma delimited string (or array) of arguments, specifying user account(s). * * @throws UserListException * If any input is unmatched, an exception is thrown. * * @return \Drush\User\UserSingleBase[] * An associative array of UserSingleBase objects, keyed by user id. */ public static function getFromParameters($inputs) { $accounts = array(); $userversion = drush_user_get_class(); if ($inputs && $userversion) { $inputs = _convert_csv_to_array($inputs); foreach($inputs as $input) { if (is_numeric($input) && $account = $userversion->load_by_uid($input)) { } elseif ($account = $userversion->load_by_name($input)) { } elseif ($account = $userversion->load_by_mail($input)) { } else { // Unable to load an account for the input. throw new UserListException('Unable to find a matching user for ' . $input . '.'); } // Populate $accounts with a UserSingle object. Will go into $this->accounts. $single = drush_usersingle_get_class($account); $accounts[$single->id()] = $single; } } return $accounts; } /* * A comma delimited list of names built from $this->accounts. */ public function names() { $names = array(); foreach ($this->accounts as $account) { $names[] = $account->getUsername(); } return implode(', ', $names); } } account->uid); } } account->uid)); } public function unblock() { user_user_operations_unblock(array($this->account->uid)); } public function addRole($rid) { user_multiple_role_edit(array($this->account->uid), 'add_role', $rid); } public function removeRole($rid) { user_multiple_role_edit(array($this->account->uid), 'remove_role', $rid); } function info() { $userinfo = (array)$this->account; unset($userinfo['data']); unset($userinfo['block']); unset($userinfo['form_build_id']); foreach (array('created', 'access', 'login') as $key) { $userinfo['user_' . $key] = format_date($userinfo[$key]); } $userinfo['user_status'] = $userinfo['status'] ? 'active' : 'blocked'; return $userinfo; } function password($pass) { user_save($this->account, array('pass' => $pass)); } public function getUsername() { return $this->account->name; } public function id() { return $this->account->uid; } } account = $account; } /** * A flatter and simpler array presentation of a Drupal $user object. * * @return array */ public function info() { return array( 'uid' => $this->account->id(), 'name' => $this->account->getUsername(), 'password' => $this->account->getPassword(), 'mail' => $this->account->getEmail(), 'user_created' => $this->account->getCreatedTime(), 'created' => format_date($this->account->getCreatedTime()), 'user_access' => $this->account->getLastAccessedTime(), 'access' => format_date($this->account->getLastAccessedTime()), 'user_login' => $this->account->getLastLoginTime(), 'login' => format_date($this->account->getLastLoginTime()), 'user_status' => $this->account->get('status')->value, 'status' => $this->account->isActive() ? 'active' : 'blocked', 'timezone' => $this->account->getTimeZone(), 'roles' => $this->account->getRoles(), 'langcode' => $this->account->getPreferredLangcode(), 'uuid' => $this->account->uuid->value, ); } /** * Block a user from login. */ public function block() { $this->account->block(); $this->account->save(); } /** * Unblock a user from login. */ public function unblock() { $this->account->get('status')->value = 1; $this->account->save(); } /** * Add a role to the current user. * * @param $rid * A role ID. */ public function addRole($rid) { $this->account->addRole($rid); $this->account->save(); } /** * Remove a role from the current user. * * @param $rid * A role ID. */ public function removeRole($rid) { $this->account->removeRole($rid); $this->account->save(); } /** * Block a user and remove or reassign their content. */ public function cancel() { if (drush_get_option('delete-content')) { user_cancel(array(), $this->id(), 'user_cancel_delete'); } else { user_cancel(array(), $this->id(), 'user_cancel_reassign'); } // I got the following technique here: http://drupal.org/node/638712 $batch =& batch_get(); $batch['progressive'] = FALSE; batch_process(); } /** * Change a user's password. * * @param $password */ public function password($password) { $this->account->setPassword($password); $this->account->save(); } /** * Build a one time login link. * * @param string $path * @return string */ public function passResetUrl($path = '') { $url = user_pass_reset_url($this->account) . '/login'; if ($path) { $url .= '?destination=' . $path; } return $url; } /** * Get a user's name. * @return string */ public function getUsername() { return $this->account->getUsername(); } /** * Return an id from a Drupal user account. * @return int */ public function id() { return $this->account->id(); } } getCurrentUserAsAccount()); } /** * Set the current "global" user account in Drupal. * @param * A user object. */ public function setCurrentUser($account) { global $user; $user = $account; } }                              .,.                                                                            .cd:..                                                                                  .xXd,,'..                                                                             .lXWx;;;,,..                                                                       .,dXWXo;;;;;,,..                                                               .;dKWWKx:;;;;;;;,,'..                                                                .;oOXNXKOo;;;;;;;;;;;;,,,'..                                                    .:dOXWMMN0Okl;;;;;;;;;;;;;;;;,,,'..                                            .,lk0NMMMMMMNKOxc;;;;;;;;;;;;;;;;;;;;,,,'..                                   .'cx0XWMMMMMMMWX0kd:;;;;;;;;;;;;;;;;;;;;;;;;;,,,..                              .'cx0NMMMMMMMMMWX0Oxl;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'..                        .;d0NMMMMMMMMMMWX0Oxl:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,...                       .:kXWMMMMMMMMMWNK0kdl:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'..                 .cONMMMMMMMMMWNX0Okoc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'.              .;kNMMMMMMMMWNX0Okdl:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'..             .oXMMMMMMWWXK0Oxdl:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'..           ,oKWWWWNXKK0kxoc:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'..          'lOO0000OOxdlc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'''.         .,lxkxxdolc:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,''''..      .,;;;::;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'''''..     .,;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'''''''.    .';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'''''''..  .',;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'''''''''.. .,;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,''''''''''.. ',;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,''''''''''''. ,,;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,''''''''''''''. ,;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'''''''''''''''. ,;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'''''''''''''''''' ,;;;;;;;;;;;;;;;;;;;;;;;;;;;cldxkkOkkxxdlc;;;;;;;;;;;;;;;;;;;;;;;;,,''''''''''','''''''' ,;;;;;;;;;;;;;;;;;;;;;;;:ox0XWMMMMMMMMMWNX0kxdc;;;;;;;;;;;;;;;;,,,'''''''';cdk00Okl,'''' ,;;;;;;;;;;;;;;;;;;;;;cxKWMMMMMMMMMMMMMMMMMMMWN0xl;;;;;;;;;;,,,'''''''';lkKWMMMMMMW0c''' ',;;;;;;;;;;;;;;;;;;:dKWMMMMMMMMMMMMMMMMMMMMMMMMMN0xl;;;;;,,'''''''';okXWMMMMMMMMMMM0:'. .,;;;;;;;;;;;;;;;;;:kNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN0xl;''''''',:oOXWMMMMMMMMMMMMMMNd'. .',;;;;;;;;;;;;;;;;xWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN0xolccoxONMMMMMMMMMMMMMMMMMMWd'.  .,;;;;;;;;;;;;;;;oXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNWMMMMMMMMMMMMMMMMMMMMMMXl..  .',;;;;;;;;;;;;;;xNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWX0OOOKNMMMMMMMMMMMMMMMMMMMM0;.    .',;;;;;;;;;;;;;dNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWN0xl:,''',cxXWMMMMMMMMMMMMMMMMWd.      .',;;;;;;;;;;;;lKMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKko:,'''''''''':dKWMMMMMMMMMMMMMWk,       .',;;;;;;;;;;;;dXMMMMMMMMMMMMMMMMMMMMMMMMWX0xl;'''',;:::;,''''';oKWMMMMMMMMMMWO,        ..',,;;;;;;;;;;o0NMMMMMMMMMMMMMMMMMMN0xdo:,''',cdO0XXXXK0kl,'''';o0WMMMMMMMNd,          .''',,,,,,,,,,;lk0XWMMMMMMMMWNX0ko:,'''''';oONN0xdoodx0NNx,''''';lOXWWWXkc.             ..'''''''''''''',:lloodddoolc;'''''''''',xN0dc,'''''',oK0:''''''',:clc;..             ...''''''''''''''''''''''''''''''''''''':c,''''''''''';;''''''''''''..               ..''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''...                  ...'''''''''''''''''''''',lxdc,'''''''''''''''''',:oxkl''''''..                       ...''''''''''''''''''':kNMNK0OxdollcccclllodxO0XX0d;'''..                         ..'''''''''''''''''',cxOKXXNWMMWWWWWWWWNXKOxo:'''..                                      ...'.'''''''''''''''',;:clooodddoollc:,'''...                                            ....'''''''''''''''''''''''''''.....                                                   ....'..''''''''''''........                               .,. .cd:.. .xXd,,'.. .lXWx;;;,,.. .,dXWXo;;;;;,,.. .;dKWWKx:;;;;;;;,,'.. .;oOXNXKOo;;;;;;;;;;;;,,,'.. .:dOXWMMN0Okl;;;;;;;;;;;;;;;;,,,'.. .,lk0NMMMMMMNKOxc;;;;;;;;;;;;;;;;;;;;,,,'.. .'cx0XWMMMMMMMWX0kd:;;;;;;;;;;;;;;;;;;;;;;;;;,,,.. .'cx0NMMMMMMMMMWX0Oxl;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'.. .;d0NMMMMMMMMMMWX0Oxl:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,... .:kXWMMMMMMMMMWNK0kdl:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'.. .cONMMMMMMMMMWNX0Okoc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'. .;kNMMMMMMMMWNX0Okdl:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'.. .oXMMMMMMWWXK0Oxdl:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'.. ,oKWWWWNXKK0kxoc:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'.. 'lOO0000OOxdlc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'''. .,lxkxxdolc:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,''''.. .,;;;::;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'''''.. .,;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,'''''''. .';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'''''''.. .',;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'''''''''.. .,;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,''''''''''.. ',;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,''''''''''''. ,,;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,''''''''''''''. ,;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'''''''''''''''. ,;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;,,'''''''''''''''''' ,;;;;;;;;;;;;;;;;;;;;;;;;;;;cldxkkOkkxxdlc;;;;;;;;;;;;;;;;;;;;;;;;,,''''''''''','''''''' ,;;;;;;;;;;;;;;;;;;;;;;;:ox0XWMMMMMMMMMWNX0kxdc;;;;;;;;;;;;;;;;,,,'''''''';cdk00Okl,'''' ,;;;;;;;;;;;;;;;;;;;;;cxKWMMMMMMMMMMMMMMMMMMMWN0xl;;;;;;;;;;,,,'''''''';lkKWMMMMMMW0c''' ',;;;;;;;;;;;;;;;;;;:dKWMMMMMMMMMMMMMMMMMMMMMMMMMN0xl;;;;;,,'''''''';okXWMMMMMMMMMMM0:'. .,;;;;;;;;;;;;;;;;;:kNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN0xl;''''''',:oOXWMMMMMMMMMMMMMMNd'. .',;;;;;;;;;;;;;;;;xWMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN0xolccoxONMMMMMMMMMMMMMMMMMMWd'. .,;;;;;;;;;;;;;;;oXMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWNWMMMMMMMMMMMMMMMMMMMMMMXl.. .',;;;;;;;;;;;;;;xNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWX0OOOKNMMMMMMMMMMMMMMMMMMMM0;. .',;;;;;;;;;;;;;dNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWN0xl:,''',cxXWMMMMMMMMMMMMMMMMWd. .',;;;;;;;;;;;;lKMMMMMMMMMMMMMMMMMMMMMMMMMMMMMWKko:,'''''''''':dKWMMMMMMMMMMMMMWk, .',;;;;;;;;;;;;dXMMMMMMMMMMMMMMMMMMMMMMMMWX0xl;'''',;:::;,''''';oKWMMMMMMMMMMWO, ..',,;;;;;;;;;;o0NMMMMMMMMMMMMMMMMMMN0xdo:,''',cdO0XXXXK0kl,'''';o0WMMMMMMMNd, .''',,,,,,,,,,;lk0XWMMMMMMMMWNX0ko:,'''''';oONN0xdoodx0NNx,''''';lOXWWWXkc. ..'''''''''''''',:lloodddoolc;'''''''''',xN0dc,'''''',oK0:''''''',:clc;.. ...''''''''''''''''''''''''''''''''''''':c,''''''''''';;''''''''''''.. ..''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''... ...'''''''''''''''''''''',lxdc,'''''''''''''''''',:oxkl''''''.. ...''''''''''''''''''':kNMNK0OxdollcccclllodxO0XX0d;'''.. ..'''''''''''''''''',cxOKXXNWMMWWWWWWWWNXKOxo:'''.. ...'.'''''''''''''''',;:clooodddoollc:,'''... ....'''''''''''''''''''''''''''..... ....'..''''''''''''........ @echo off SET SCRIPT_HOME=%~dp0 SET PATH=%SCRIPT_HOME%php;%PATH% @php.exe %SCRIPT_HOME%composer.phar %* @echo off SET SCRIPT_HOME=%~dp0 SET PHP_PATH=%SCRIPT_HOME%php SET PATH=%SCRIPT_HOME%tools\bin;%PHP_PATH%;%PATH% @php.exe "%SCRIPT_HOME%vendor\drush\drush\drush.php" --php="php.exe" %* MZ@ !L!This program cannot be run in DOS mode. $@@@ͱ&VSͱ&TJͱ&W@ͼ E WA UARich@PELz(R  b@ @T<D 0@.text`b `.rdataEFf@@.data,@.reloc@BUQj@EjjEP @PMQUR@]U EEEPhjh@jjh@E}uEh @E]jh@jYMZf9@t33<@@PEu f9@u3ۃt@v 9@É] ujY.ujYre yjY@@@N yjYjyj YjYtPY @@P5@5@ u܅uV.MEQPYYËeuu܃}uVREU=`@tuhYY]{UE8csmu%xu@= t=!t="t =@t3]h @.Y3UVEV\W}99t ;r;s99t3ɅQu a3@uE S^`F`yj$_F\ d|9~du Fd9u Fdu9u Fdd9u FdS9u FdB9u Fd19u Fd 9u Fd9uFdvdjY~d qaY^`[3_^]jh@u~$t v$\Y~,t v,MY~4t v4>Y~<t vj Y}E Flu@FlvlRYE 3Guj Yj Yuc3h@CY4@tVhjYYt-V54@sYYtjVYY(@N3@^3^á4@tP 4@Y8UQEPh@j8@th@u<@tuUuYu4@UuYhjjj@ jjj1 U=@th@N!Yt u@Ye#h0@h@YYuPVWh"@/#Y@@ tЃ;r=@_^th@ Yt jjj@3]Ujju ]Vj,@V#VPV#V&#V:#Vi ^-UVu tЃ;u r^]UVu3utу;u r^]j`YjYjh@ jBYe=@@E@} 5@50@֋؉]ԅtt5@֋]}}܃};rWj,@9t;rG7֋j,@5@50@։E5@֋M9Mu9EtMى]ԉEhD@h4@YYhL@hH@YYE }u)@jYui}tjY Ujju ]jYtjYu= @uh1h'YYUM3; Š@t @r3]ËŤ@]U@3ʼnEVuWVYySjhYjWYu = @Ah@hh(@# 3ۅ/hhZ@Sfb@H@uh@VhZ@# hZ@#@Yt>tF FFujX CP@tEtAWP@t6>%uF @ u F FhF PT@F"F @F\@t @C"u3FȉM"5tGEPF%Yt tGFEtMu< t< utGNe>< t< uF>U t:U E3B3FA>\t>"u3u}t F8"u 339EE It\GutA9Mu< t8< t4t*P.$YttGFGtFFotG-U _^[t"E]Ã= @u V5@W3u<=tGV}FYuGjPw YY= @tʋ5@S>t>VH>=YXt"jSF YYt@VSPu# uH>uȋ5@V%@'@3Y[_^5 @% @3PPPPPUE @]UEx!~ u `@ `@`@]]U@eeVWN@;t t У@fEPl@E3EE(@1Eh@1EEPd@M3ME3M3;uO@u G ȉ @щ @_^VW@@ tЃ;r_^VW@@ tЃ;r_^UQWp@3tuVf9tf9uf9uSPPP+PFVWPPx@Et7PYt*3PPuSVWPPx@u S Y3Wt@ Wt@3[^_h#@d5D$l$l$+SVW@1E3PeuEEEEdËMd Y__^[]QUS] VW{3=@EEst O3 0O G3 0uE@fEEEECC E@@LEt{2M~~hE8csmu(=@th@3tju@UM E U9P th@VE X tufMÉ]]tG!E{ t6h@V˺t O3 0lO W3 2\E_^[]ËO3 0EO G3 05M֋IEjhP@@xte3@ËeEhE%@,@d@U졀@3@tu]]%@U졄@3@ut]@]U졈@3@ut]@]U졌@3@u ut]@]UQV5@y%@33@ut VMQЃzuF5@3^VWht@@5<@h@W3@h@W@3@h@W@3@h@W@3@h@W@3@h،@W@3@h@W@3@h@W@3@h@W@3@h0@W@3@hP@W@3@hh@W@3@h@W@3@h@W@3@h@W@3@@hč@W3@h@W@3@h@W@3@h @W@3@h4@W@3@hP@W@3@hd@W@3@ht@W@3@h@W@3@h@W@3@h@W@3@h@W@3@hԎ@W@3@h@W@3@_@^Uu@]Uu@P@]Uj@u|@]UVu<@uVqYuj!Y4@@^]VW@SttSX@S"'Y@|[>t~u6X@@|_^jhp@=P@uLjh{YY}<@u[j2Yu\ 3Aj Ye<@uhVT@4@ViYE 3@j 7YVW@h@~u>h6T@@|3_@^UE4ň@@]U}t-uj5P@@uV @P Y^]UVW3ju u u'9@vV@; @vuË_^]USVW=@3uwYu%t!V@=@;vu_^[]UVW3u uYYu,9E t'9@vV@;@vu_^]USV5 @W}Wփxtwx֋tPփ|tw|֋tPjX_E{@t ;t3֋E{t{ts֋EHEu΋P_^[]USVu3ۋWtf=0@t_FxtX9uTt9uPYYF|t9uPYYvxYYtD9u@-P+P+P=@t9uPjYYjX~E@tt8uP?38YYEtGt 8uPYEHEuVY_^[]UVuSW=$@V׃~xtvx׋tP׃~|tv|׋tPjX^E{@t ;t3׋E{t{ts׋EHEu΋Q_[^]j h@q $@Npt"~ltpluj Yj Ye5@FlP!YYuE뼋uj YUW} t;Et4V0;t(W8YtV>Yu@tVOY^3_]Ã= @ujY @3UVMF ufЉVJlJhN; @t$@BpuF;@tN$@ApuFNApuApF  AF^]UE-t&t tHt3]á`@]á\@]áX@]áT@]UMj.E%@u@@,u@@uE@@}tMapUS]VWh3sWV`3ȉ{{ { @ +7FIu9AJu_^[]U @3ʼnESVuWPv@3ۿÈ@;rƅ Q ;s Ƅ @;vuSvPWPjS/SvWPWPWS@SvWPWPhS$MtL tL A;rWjX+‹ˉ‰ w LA w L AA;rM_^3[j h@g $@Optltwhuj Y~j Yewhu;5@t6tV$@u@tVY@Gh5@uV @E뎋uj Yjhн@W؉]=shuYE;Fnh Y؅[Eph33SuGYY} Eph$@EuHh@t QYEXhS @E@p$@j sYuC@C@@ΉM}fDK fM@AΉM} D@Au}@F5@$@u@=@tP\Y@S @E1}j !Y#u@tSY3fU @3ʼnESVuu -Y]uVY3W3ωM9@A0M=rP@EPS@hFWP^3C 9]vO}Et!PtLA;v8uߍF@Iuv^~3ȋ ~ 9=@t VhFWPU k0@E8t5At+s@DAC;v9u΋EGEr]S^FWjN @_ffRIOuVPSYYt1P,@@u,@KQ,@@E3_^[UuYH]V3h@,@h@(r^U5@0@tuYt3@]3]UE@]UE@]UE@]5@0@UE@@@@]j$h0@3ۉ]3}؋u PtjY+t"+t+t^+uHk}؅udE@@^w\VSYYEVƃt6t#Ht E@@E@@ E@@3C]P0@E܃ujtjxYet tuG`EЃg`uAGdEGdu/ @щUԡ@;}&k G\dBUԋ @j,@MEu wdVUYu]}؅tjtIut+fRftIu3ufj"UVutU t Mu3fuj^0^]W+fIftJu3_uf@j"UEffu+EH]UUMVu u 9M u&33tE tu3fuu3fj^0h^]SWًu+f3vft%Ou +f[ftOtJuu3f_[{uE 3jPfTAX3fij"U$@3ʼnEES,@VWEE 3WEӋuJE9=@hWh@Ā@u$@Whh@̀@Sh@V<@?Ph@V@<@PhȒ@V@<@Phܒ@V @<@Pӣ@th@V<@Pӣ@u@tEtPȀ@9}tjX9}t5@0@j@0@;tO95@tGP5@EӋMEt/t+хtMQj MQjPUtEu u 0@;t$PӅtЋt @;t PӅtWЋu5@Ӆt VuuW3M_^3[; @uSVWT$D$L$URPQQhB@d5@3ĉD$d%D$0XL$,3p t;T$4t;v.4v\ H {uhC"C4d_^[ËL$At3D$H3 ]D$T$UL$)qqq( ]UVWS33333[_^]Ëj33333USVWjRhB@Q._^[]Ul$RQt$ ]`@Vj^u;}ƣ`@jPvYY\@ujV5`@]YY\@ujX^3ҹ@  R @}\@3^=@t5\@%\@YUVu@;r"@w+P*N Y F P@^]UE}PE YH ]ËE P@]UE@;r=@w` +PY]à P@]UME }` APY]à P@]UVuMdu EML0u39UtEp#E…t3B}^tMapUjjuj]UVutU t Mu!j^0^]W+AtJu_u j"3/tjMY @t!j+tjY)jh@j jUVuwoSWP@uJjhyP@YYt3AQjPԀ@u&j [9H@t VDYu=6_[V#Y" 3^]U}u u ]Y]Vu u u;Y3MS0uFVuj5P@؀@؅u^9H@t@VYtvVY 3[^]@PY@PYUVutj3X;E sP 3Qu uF3ɃwVj5P@Ԁ@ȅu*=H@tV YuЋEt봋Et ^]UVuF ;<@tPYF;@@tPYF;D@tPYF;H@tPYF;L@tPYF ;P@tPYF$;T@tPYF8;h@tPYF<;l@tPYF@;p@tPxYFD;t@tPfYFH;x@tPTYFL;|@tPBY^]UVutY;0@tP#YF;4@tPYF;8@tPYF0;`@tPYF4;d@tPY^]UVunvvv vvv6v v$v(yv,qv0iv4avYv8QvvD6vH.vL&vPvTvXv\v`vdvhvlvpvtvxv|@vk`UJ?4)@vk`@R G<1& $(,048<@D@HLPT~Xs\h`]^]UQQ@3ʼnESVuW~!EI8t@u+H;ƍp|M$3u E@E$39E(jjVuPQ@@ȉMu3X~Kj3Xr? Mw܅tQYt M3ۅtQSVuju$@@ujjVSuu Mt,M ;QuVSuu ~Bj3Xr6};wtfPYtQ3t@WVuSuu +t!3PP9E uPPu uWVPu$x@V`YSYYǍe_^[M3UuMu(Eu$u uuuuu P$}tMapUEt8uPY]UQ@3ʼnEMSVW3u E@E39E WWuuPQ@@؅u3~Aw9]=wtPYttPWV SVuuju@@tuPVu ܀@V YǍe_^[M3WUuMu Euuuuu P}tMap̋T$ L$tD$%T@s L$ W|$]T$ |%@ Wr1كt +шuʃtt uD$_ËD$Ã%X@U}uo]uj5P@@]jYUSVW3;+‹jU48@u ty^~;~Ѓ<@_^[]U}tuYx=s @]3]U@3@t3QQQuuuuu u]uuuuu uYP@]UVu3t^M SW}jA[jZZ+UjZZf;r f;w f;r f;Ew Nt ftf;t_+[^]WVt$L$|$ ;v;h%T@s3Ʃu%@%T@s vs~vftcfoNvfo^0foF fon0v00fof: ffof: fGfof: fo 0}v foNvIfo^0foF fon0v00fof:ffof:fGfof:fo 0}vVfoNvfo^0foF fon0v00fof:ffof:fGfof:fo 0}v|ovfs vs~vfT@ur*$T@Ǻr $S@$T@$+~~.WvVYP ;uF yF N _Nf^[]jYjhP@e3}!}jY!}3]u;5`@\@t]@ tWPVYYE\@@ t0uPYtG}u@ tPYu E܃e F녋]}u\@4VYYEtEË]}jaYU@jD@u=D@YYujYh Y]U$jtjY)(@ $@ @@5@=@f@@f 4@f@f @f%@f-@8@E,@E0@E<@x@0@4@(@ ,@8@jXkǀ<@jXk @LjX @Lh@USVWUjjh[@u]_^[]ËL$At2D$H3,UhP(RP$R]D$T$SVWD$UPjh [@d5@3PD$dD$(Xp t:|$,t;t$,v-4v L$ H |uhDID_뷋L$d _^[3d y [@uQ R 9QuSQ@ SQ@L$ KCk UQPXY]Y[jhx@ 3}joY!}j^u;5`@}S\@tD@ tPB YtG}|)\@ PX@\@4Y\@$FE Ë}jPYjh@p@95@t*j YeVh@KYY@Eyj YQL$+ȃ Y QL$+ȃ Y uffnf`fafpSQكuxڃt0ffAfA fA0fA@fAPfA`fApKuЅt7tIfIKutt f~IJutAKuX[ۃ+RӃtAJut f~IKuZ^U%P@S3C @j r3ɋÉP@V5@W}_OW E5@tP@5@EtP@5@j3Xu^NV E5T@t 5T@33}_OW }Genuu_}ineIuV}nteluM3@3_OW E%?=t#=`t=pt=Pt=`t=pu 5T@_^3[jh@}uy ;=@E߃X@DttW Y3uEX@Dt(W YP@u@ut0 uE !}uW Y FSjhؾ@uu_ ;5@X@D8tcV YeX@D8tuu V_  }E )u}V Y dqU @3ʼnEEM 3W@D<,9Uu3uG!8tSV0 X@\$tu+EШu!8L@D t jRRPS @Y0X@D軲@l39P0X@4@@9@t@D!$ʉ49}~38Dž  3 @0X@|8tD4EjEMd8PZP> YtDD4+EjR

    1@f;uj Yf @f3rH+j(PSHP0,X@4D@8<(D<9(@+;E ]$Dž $+ʋH;s;>$f;uj ^f0$f8r3VVhU QH++PPVhx@8<43ɉ@j+(RPP0X@4D@t@(4@;@@48;Q$D+<;7j(QuD4D@t (3@Duct$j[;u ?VY60X@D@t :u3  +,^[M3_UVuWu EF t9VVV(P|y~t vfYf _^]j h@艹}3uu裹F @tf VgYeV?Y}ENju}VYQL$+#ȋ%;r Y$-jh@ٸ}4X@~u0j #Ye~uhF PT@FE*X@ P@3@諸Ë}j -YUEVWx`;@sX X@Dt=<t7= @u3+tHtHuQjQjQj@X@ 3 H _^]UMu. Z Bx&; @sX@Dt]  ]UMX@ P@]Uu MEMH%}tMapUjuYY]UQQVuWVY;u NjDuMQuu P@u@P0YӋX@d0EU_^USVu t]t>uEt3f3^[WuMEuMtf3GEPPYYt@}t~';_t|%39EPuwtVj w@@}u ;_tr.~t(t139E3GPuEWVj p@@uX*}tMap_6Ujuu u]UQ@u @ujMQjMQP@tfEjh8@6uu xy;5@sqX@D8tSVYeX@D8t VUYb }E )u}V/Y + 辴UVW}WYtPX@u u u@DtjljcYY;tWWYP@u @3WY X@D9t VhY3_^]UVuF t F tv誺f 3YFF^]á@t tP@3PPjPjh@h @@@VD$ u(L$D$ 3؋D$d$ȋd$Gȋ\$T$ D$ ud$ȋD$r;T$ wr;D$v N+D$T$3+D$T$ ؃ʋӋًȋ^̋D$L$ ȋL$ u D$S؋D$d$؋D$[%@%Ѐ@d 8Pfv<Tf|,J^r~$0D`r"2FV.@9@B@$^@M@\@o@EC@   mscoree.dllCorExitProcessR6008 - not enough space for arguments R6009 - not enough space for environment R6010 - abort() has been called R6016 - not enough space for thread data R6017 - unexpected multithread lock error R6018 - unexpected heap error R6019 - unable to open console device R6024 - not enough space for _onexit/atexit table R6025 - pure virtual function call R6026 - not enough space for stdio initialization R6027 - not enough space for lowio initialization R6028 - unable to initialize heap R6030 - CRT not initialized R6031 - Attempt to initialize the CRT more than once. This indicates a bug in your application. R6032 - not enough space for locale information R6033 - Attempt to use MSIL code from this assembly during native code initialization This indicates a bug in your application. It is most likely the result of calling an MSIL-compiled (/clr) function from a native constructor or from DllMain. R6034 - inconsistent onexit begin-end variables DOMAIN error SING error TLOSS error runtime error X@@ p@ Ȃ@@h@ȃ@@h@؄@(@@@T@@ `@!ȇ@"@x @y@@z\@x@@R6002 - floating point support not loaded Runtime Error! Program: <program name unknown>... Microsoft Visual C++ Runtime Librarykernel32.dllFlsAllocFlsFreeFlsGetValueFlsSetValueInitializeCriticalSectionExCreateSemaphoreExWSetThreadStackGuaranteeCreateThreadpoolTimerSetThreadpoolTimerWaitForThreadpoolTimerCallbacksCloseThreadpoolTimerCreateThreadpoolWaitSetThreadpoolWaitCloseThreadpoolWaitFlushProcessWriteBuffersFreeLibraryWhenCallbackReturnsGetCurrentProcessorNumberGetLogicalProcessorInformationCreateSymbolicLinkWSetDefaultDllDirectoriesEnumSystemLocalesExCompareStringExGetDateFormatExGetLocaleInfoExGetTimeFormatExGetUserDefaultLocaleNameIsValidLocaleNameLCMapStringExGetCurrentPackageIdSunMonTueWedThuFriSatSundayMondayTuesdayWednesdayThursdayFridaySaturdayJanFebMarAprMayJunJulAugSepOctNovDecJanuaryFebruaryMarchAprilJuneJulyAugustSeptemberOctoberNovemberDecemberAMPMMM/dd/yydddd, MMMM dd, yyyyHH:mm:ssSunMonTueWedThuFriSatSundayMondayTuesdayWednesdayThursdayFridaySaturdayJanFebMarAprMayJunJulAugSepOctNovDecJanuaryFebruaryMarchAprilJuneJulyAugustSeptemberOctoberNovemberDecemberAMPMMM/dd/yydddd, MMMM dd, yyyyHH:mm:ssen-USd@p@|@@ja-JPzh-CNko-KRzh-TWUSER32.DLLMessageBoxWGetActiveWindowGetLastActivePopupGetUserObjectInformationWGetProcessWindowStation ((((( H h(((( H H  !"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\]^_`abcdefghijklmnopqrstuvwxyz{|}~  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ{|}~X@`@h@p@@@@@ @ @ @ @ @ȩ@Щ@ة@@@@@@@@@ @(@0@8@@@H@ P@!X@"`@#h@$p@%x@&@'@)@*@+@,@-@/@6@7Ȫ@8Ъ@9ت@>@?@@@A@C@D@F@G@I @J(@K0@N8@O@@PH@VP@WX@Z`@eh@p@t@@@@@@@@ H@ ȫ@ ԫ@ @@@@d@|@@@(@4@@@L@X@d@p@|@@@ @!@"@#Ĭ@$Ь@%ܬ@&@'@)@* @+@,$@-<@/H@2T@4`@5l@6x@7@8@9@:@;@>@?̭@@ح@A@C@D@E@F @G,@I8@JD@KP@L\@Nh@Ot@P@R@V@W@Z@eĮ@kԮ@l@@@p@@ @  @ ,@8@D@P@\@h@@,@;@>@C@kԯ@ @ @ @ @ @ @ ,@; D@k P@`@l@x@ @ @ @@;@İ@а@ܰ@ @ @ @ @;$@4@ @@ L@ X@d@;|@@ @ @@;ȱ@ ر@ @ @; @$ @ $@ $$@;$0@(@@ (L@ (X@,d@ ,p@ ,|@0@ 0@ 0@4@ 4@ 4IJ@8в@ 8ܲ@<@ <@@@ @ @ D@ H$@ L0@ P<@|H@|X@p@B@,`@qX@l@x@@@@@@@̳@س@@@@C@@ @@),@D@kh@!\@c`@h@Dt@}@h@@E@@G@@@H@ȴ@Դ@@I@@h@A@@@J@ @,@8@D@P@\@h@t@@@@K@@@ @ȵ@Ե@@@@@@@(@4@@@L@X@d@p@|@@@x@#@e@*@l@&@h@ Ķ@LЪ@.ж@s@ ܶ@@@@M @@P@>$@@70@@ <@Nت@/H@t @T@`@Zȩ@ l@O@(x@jX@@aЩ@@Pة@@@Q@@RȪ@-@r@1̷@x0@:ط@@X@?@@S@2@y@% @g@$@f$@@+0@m<@H@=H@8@;T@@0`@l@wx@u@U@@@T@@@@6@~@̸@V@ظ@W@@@@@ @X@,@Y@@<8@D@P@v\@(@h@[p@"t@d@@@@@й@0@@\X@@@@4@8@L@X@]@3d@z`@@p@ @8@(@9@@@@^@nH@@_@5@|`@ Ⱥ@bP@Ժ@`@4@@{@'@i@o(@8@H@T@`@l@x@F@parbgcazh-CHScsdadeelenesfifrhehuisitjakonlnoplptroruhrsksqsvthtruridukbesletlvltfavihyazeumkafkafohimskkkyswuzttpagutateknmrsamnglkoksyrdivar-SAbg-BGca-EScs-CZda-DKde-DEel-GRfi-FIfr-FRhe-ILhu-HUis-ISit-ITnl-NLnb-NOpl-PLpt-BRro-ROru-RUhr-HRsk-SKsq-ALsv-SEth-THtr-TRur-PKid-IDuk-UAbe-BYsl-SIet-EElv-LVlt-LTfa-IRvi-VNhy-AMaz-AZ-Latneu-ESmk-MKtn-ZAxh-ZAzu-ZAaf-ZAka-GEfo-FOhi-INmt-MTse-NOms-MYkk-KZky-KGsw-KEuz-UZ-Latntt-RUbn-INpa-INgu-INta-INte-INkn-INml-INmr-INsa-INmn-MNcy-GBgl-ESkok-INsyr-SYdiv-MVquz-BOns-ZAmi-NZar-IQde-CHen-GBes-MXfr-BEit-CHnl-BEnn-NOpt-PTsr-SP-Latnsv-FIaz-AZ-Cyrlse-SEms-BNuz-UZ-Cyrlquz-ECar-EGzh-HKde-ATen-AUes-ESfr-CAsr-SP-Cyrlse-FIquz-PEar-LYzh-SGde-LUen-CAes-GTfr-CHhr-BAsmj-NOar-DZzh-MOde-LIen-NZes-CRfr-LUbs-BA-Latnsmj-SEar-MAen-IEes-PAfr-MCsr-BA-Latnsma-NOar-TNen-ZAes-DOsr-BA-Cyrlsma-SEar-OMen-JMes-VEsms-FIar-YEen-CBes-COsmn-FIar-SYen-BZes-PEar-JOen-TTes-ARar-LBen-ZWes-ECar-KWen-PHes-CLar-AEes-UYar-BHes-PYar-QAes-BOes-SVes-HNes-NIes-PRzh-CHTsraf-zaar-aear-bhar-dzar-egar-iqar-joar-kwar-lbar-lyar-maar-omar-qaar-saar-syar-tnar-yeaz-az-cyrlaz-az-latnbe-bybg-bgbn-inbs-ba-latnca-escs-czcy-gbda-dkde-atde-chde-dede-lide-ludiv-mvel-gren-auen-bzen-caen-cben-gben-ieen-jmen-nzen-phen-tten-usen-zaen-zwes-ares-boes-cles-coes-cres-does-eces-eses-gtes-hnes-mxes-nies-paes-pees-pres-pyes-sves-uyes-veet-eeeu-esfa-irfi-fifo-fofr-befr-cafr-chfr-frfr-lufr-mcgl-esgu-inhe-ilhi-inhr-bahr-hrhu-huhy-amid-idis-isit-chit-itja-jpka-gekk-kzkn-inkok-inko-krky-kglt-ltlv-lvmi-nzmk-mkml-inmn-mnmr-inms-bnms-mymt-mtnb-nonl-benl-nlnn-nons-zapa-inpl-plpt-brpt-ptquz-boquz-ecquz-pero-roru-rusa-inse-fise-nose-sesk-sksl-sisma-nosma-sesmj-nosmj-sesmn-fisms-fisq-alsr-ba-cyrlsr-ba-latnsr-sp-cyrlsr-sp-latnsv-fisv-sesw-kesyr-syta-inte-inth-thtn-zatr-trtt-ruuk-uaur-pkuz-uz-cyrluz-uz-latnvi-vnxh-zazh-chszh-chtzh-cnzh-hkzh-mozh-sgzh-twzu-za(@x@  !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ACONOUT$H@@#B [@@@@@@@|~@e%@i%@)@C.@v2@3@)9@<9@9@<@Y@~Y@\@]@`@`@$j@j@n@rd 8Pfv<Tf|,J^r~$0D`r"2FVSendMessageTimeoutAUSER32.dllGetStdHandlelstrlenAWriteConsoleAGetCommandLineAjGetLastErrorSetLastErrorqInterlockedIncrementmInterlockedDecrement(GetCurrentThreadId<EncodePointerDecodePointermExitProcessGetModuleHandleExWGetProcAddressMultiByteToWideCharWriteFile}GetModuleFileNameWGetProcessHeapWGetFileTypefInitializeCriticalSectionAndSpinCountDeleteCriticalSectionGetStartupInfoW|GetModuleFileNameA<QueryPerformanceCounter$GetCurrentProcessIdGetSystemTimeAsFileTime@GetEnvironmentStringsWFreeEnvironmentStringsWWideCharToMultiByteUnhandledExceptionFilterPSetUnhandledExceptionFilter#GetCurrentProcessoTerminateProcessTlsAllocTlsGetValueTlsSetValueTlsFreeGetModuleHandleW@EnterCriticalSectionLeaveCriticalSectionQHeapFree_SleepIsValidCodePageGetACPGetOEMCPGetCPInfoIsDebuggerPresentIsProcessorFeaturePresentLoadLibraryExWOutputDebugStringWLoadLibraryWRtlUnwindMHeapAllocTHeapReAllocGetStringTypeWVHeapSizeLCMapStringWFlushFileBuffersGetConsoleCPGetConsoleMode/SetStdHandle SetFilePointerExWriteConsoleWCloseHandleCreateFileWKERNEL32.dllEnvironmentCould not broadcast WM_SETTINGCHANGE C@@@@@ @@@@$@,@8@D@L@X@\@`@d@h@l@p@t@x@|@@@@@@@h@@@@ď@Џ@؏@@@@@@@$@,@4@<@D@L@T@\@l@|@@@@Đ@ؐ@@@@@@@@@ @(@0@8@H@\@h@@t@@@@@@ԑ@@@@ @4@H@@@@@@@0@@@@@@`y!@~ڣ @ڣ AϢ[@~QQ^ _j21~ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ        ! 5A CPR S WY l m pr   )    )P@)P@)P@)P@)P@)P@)P@)P@)P@)P@N@D@@0@.,@@@@@@@@@@@ @ @ @ @ @ @ @.@@ @ 00(0P0_0t00000000!1&101j1o1v1|11N2394^4h444 5585S5k5w55555>6I6j666666666A7I7\7g7l7~7777777l88888888899969;9G9L9k999::R:j:t::::::::::: ;;D;W;;;;;<$<*9>R>>>>>>>"?(? 0001L1X1b1s1~111111 22'202=2l2t2222222313<3Q3n33\4d4{444H5z55555555555555666 686I6O6U6\6e6j6p6x6}66666666666666666666677777#7(7.767;7A7I7N7T7\7a7g7o7t7y77777777777777777777778 8888%8-82888@8E8K8S8X8^8f8k8q8y88888888888 9949=9I9T9y999999 ::+:1:G:M:_:::::::#;,;:;U;;;<>>>>>>?_?f?m?t???????00Y0t011,292C2Q2Z2d2233&393S3[3f3}333333333 474q444T5555/67O7Z7`777 8=88888889999:::::::::::::;$;1;9;?;K;P;U;Z;c;;;;;<<<< <}<<<<=????????@ 00 0&0,040:0@0H0N0T0\0e0l0t0}000000001,1E1n1112222 33%343;3L3Z3e3t3~333334<45U5t55566#6Z6r66667#757G7Y7k7}777777788,8>8P8;K<<=0>m>>>t???P 0#0N0w00000111u333333333454<4@4D4H4L4P4T4X4444445%5@5G5L5P5T5u55555555555>6D6H6L6P678899599999: :&:,:2:8:?:F:M:T:[:b:i:q:y:::::::::::: ;q;};;<<:<[7>P>Y>x>>>>>>>R?`??????`F0y0003112P2_2}223334(44 56555667-868^888 99n:::::;;.;P;W;;;;<<<>@>V>r>>*?4?P??????p 00H1 1$1(1,181<1@1::::::::::::; ;;;$;,;4;<;D;L;T;LT2X2\2`2;$;,;4;<;D;L;T;\;d;l;t;|;;;;;;;;;;;;;;;;;< <<<$<,<4<< >>>$>,>4><>D>L>T>\>d>l>t>|>>>>>>>>>>>>>>>>>? ???$?,?4?>(>H>h>t>>>>>?0?P?11111111111111111111222 22222 2$2(2,2024282<2@2D2H2L2P2T2X2d2h2l2p2t2x2|222222222222222222222222222222222333 333<3L3\3l3|3333333h:l:p:t:x:|:::::::(=0=4=8=<=@=D=H=L=P=T=`=d=h=l=p=t=x=|====@cscript //NoLogo %~dp0setenv.js @pausevar shell = WScript.CreateObject("WScript.Shell"); var fs = new ActiveXObject("Scripting.FileSystemObject"); var PATH_KEY = "HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\Path"; var path = shell.RegRead(PATH_KEY); var windrush = fs.GetParentFolderName(WScript.ScriptFullName) var inPath = path.toLowerCase().indexOf(windrush.toLowerCase()) != -1; WScript.Echo("Adding '" + windrush + "' to your PATH variable..."); if (inPath) { WScript.Echo("'" + windrush + "' is already in your PATH variable"); } else { try { shell.RegWrite(PATH_KEY, windrush + ";" + path, "REG_EXPAND_SZ"); var oExec = shell.Exec(windrush + "\\tools\\bin\\notify_env_change.exe"); while (oExec.Status == 0) WScript.Sleep(100); if (oExec.ExitCode != 0) WScript.Echo("Failed to notify the system about PATH change. Reboot required"); WScript.Echo("Done."); } catch (err) { WScript.Echo("Could not write PATH variable to the registry.\nYou may have insufficient permissions to that. Try running this script as administrator"); } } This directory holds the build script for Drush's Windows distribution. This script is only useful to Drush administrators who are generating a new build. To use this script: - Edit the metadata at the top - Run the script - Attach the .zip file to the corresponding Release on Github. - Update the links at bottom of docs/install.md #!/bin/bash ######################################## # Configurations BINTOOL_GIT=https://github.com/acquia/DevDesktopCommon.git PHP_VER=5.4.41-nts-Win32-VC9-x86 # NOTE there in also "5.5" string in the download url. Change it if upgrading to 5.6 MYSQL_VER=5.5.41-win32 # goes to composer require command DRUSH_VER=7.0.0 TOPDIR=windrush ######################################## # Utils fail() { echo "$1">&2 kill -s TERM $$ } replace_in_file() { sed "s/$1/$2/" "$3" > aa.tmp mv aa.tmp "$3" } enable_php_extension() { for a in $1 ; do replace_in_file ";extension=php_$a.dll" "extension=php_$a.dll" "$2" done } ######################################## # Prepare $TOPDIR dir [ -e $TOPDIR ] && ( rm -r -f $TOPDIR || fail "Could not remove $TOPDIR dir" ) mkdir $TOPDIR ######################################## # MSYS & other binary tools # # msys & stuff from the DD repo #svn export $BINTOOL_SVN $TOPDIR/tools || fail "Svn failed to export from $BINTOOL_SVN" [ -e DevDesktopCommon ] && rm -r -f DevDesktopCommon git clone $BINTOOL_GIT || fail "Git failed to get $BINTOOL_GIT" mv DevDesktopCommon/bintools-win/msys $TOPDIR/tools rm -r -f DevDesktopCommon # mysql MYSQL_ZIP=mysql-$MYSQL_VER.zip MYSQL_URL=http://dev.mysql.com/get/Downloads/MySQL-5.5/$MYSQL_ZIP if [ ! -e $MYSQL_ZIP ]; then wget $MYSQL_URL || fail "Could not download MySQL from $MYSQL_URL" fi unzip -o -j $MYSQL_ZIP mysql-$MYSQL_VER/bin/mysql.exe -d $TOPDIR/tools/bin unzip -o -j $MYSQL_ZIP mysql-$MYSQL_VER/bin/mysqldump.exe -d $TOPDIR/tools/bin ######################################## # PHP PHP_ZIP=php-$PHP_VER.zip PHP_URL=http://windows.php.net/downloads/releases/$PHP_ZIP if [ ! -e $PHP_ZIP ]; then wget $PHP_URL || fail "Could not download PHP from $PHP_URL" fi unzip $PHP_ZIP -d $TOPDIR/php PHP_INI=$TOPDIR/php/php.ini cp "$PHP_INI-development" "$PHP_INI" replace_in_file '; extension_dir = "ext"' 'extension_dir = "ext"' "$PHP_INI" enable_php_extension "bz2 curl fileinfo gd2 gettext intl mbstring mysql mysqli openssl pdo_mysql soap sockets tidy xmrpc xsl" "$PHP_INI" ######################################## # Drush, composer and setenv stuff cd $TOPDIR php -r "readfile('https://getcomposer.org/installer');" | php ./composer.phar require drush/drush:$DRUSH_VER || fail "Composer failed" cd .. cp assets/drush.bat $TOPDIR cp assets/composer.bat $TOPDIR cp assets/setenv.bat $TOPDIR cp assets/setenv.js $TOPDIR cp assets/notify_env_change.exe $TOPDIR/tools/bin ######################################## # Zip everything up [ -e $TOPDIR.zip ] && rm $TOPDIR.zip zip -r $TOPDIR.zip $TOPDIR cd .. echo "Done." 0;"); } /** * Add help components to a command. */ function hook_drush_help_alter(&$command) { if ($command['command'] == 'sql-sync') { $command['options']['myoption'] = "Description of modification of sql-sync done by hook"; $command['sub-options']['sanitize']['my-sanitize-option'] = "Description of sanitization option added by hook (grouped with --sanitize option)"; } if ($command['command'] == 'global-options') { // Recommended: don't show global hook options in brief global options help. if ($command['#brief'] === FALSE) { $command['options']['myglobaloption'] = 'Description of option used globally in all commands (e.g. in a commandfile init hook)'; } } } /** * Add/edit options to cache-clear command. * * @param array $types * Adjust types as needed. Is passed by reference. * * @param bool $include_bootstrapped_types * If FALSE, omit types which require a FULL bootstrap. */ function hook_drush_cache_clear(&$types, $include_bootstrapped_types) { $types['views'] = 'views_invalidate_cache'; } /** * Inform drush about one or more engine types. * * This hook allow to declare available engine types, the cli option to select * between engine implementatins, which one to use by default, global options * and other parameters. Commands may override this info when declaring the * engines they use. * * @return array * An array whose keys are engine type names and whose values describe * the characteristics of the engine type in relation to command definitions: * * - description: The engine type description. * - topic: If specified, the name of the topic command that will * display the automatically generated topic for this engine. * - topic-file: If specified, the path to the file that will be * displayed at the head of the automatically generated topic for * this engine. This path is relative to the Drush root directory; * non-core commandfiles should therefore use: * 'topic-file' => dirname(__FILE__) . '/mytopic.html'; * - topics: If set, contains a list of topics that should be added to * the "Topics" section of any command that uses this engine. Note * that if 'topic' is set, it will automatically be added to the topics * list, and therefore does not need to also be listed here. * - option: The command line option to choose an implementation for * this engine type. * FALSE means there's no option. That is, the engine type is for internal * usage of the command and thus an implementation is not selectable. * - default: The default implementation to use by the engine type. * - options: Engine options common to all implementations. * - add-options-to-command: If there's a single implementation for this * engine type, add its options as command level options. * - combine-help: If there are multiple implementations for this engine * type, then instead of adding multiple help items in the form of * --engine-option=engine-type [description], instead combine all help * options into a single --engine-option that lists the different possible * values that can be used. * * @see drush_get_engine_types_info() * @see pm_drush_engine_type_info() */ function hook_drush_engine_type_info() { return array( 'dessert' => array( 'description' => 'Choose a dessert while the sandwich is baked.', 'option' => 'dessert', 'default' => 'ice-cream', 'options' => 'sweetness', 'add-options-to-command' => FALSE, ), ); } /** * Inform drush about one or more engines implementing a given engine type. * * - description: The engine implementation's description. * - implemented-by: The engine that actually implements this engine. * This is useful to allow the implementation of similar engines * in the reference one. * Defaults to the engine type key (e.g. 'ice-cream'). * - verbose-only: The engine implementation will only appear in help * output in --verbose mode. * * This hook allow to declare implementations for an engine type. * * @see pm_drush_engine_package_handler() * @see pm_drush_engine_version_control() */ function hook_drush_engine_ENGINE_TYPE() { return array( 'ice-cream' => array( 'description' => 'Feature rich ice-cream with all kind of additives.', 'options' => array( 'flavour' => 'Choose your favorite flavour', ), ), 'frozen-yogurt' => array( 'description' => 'Frozen dairy dessert made with yogurt instead of milk and cream.', 'implemented-by' => 'ice-cream', ), ); } /** * Alter the order that hooks are invoked. * * When implementing a given hook we may need to ensure it is invoked before * or after another implementation of the same hook. For example, let's say * you want to implement a hook that would be called after drush_make. You'd * write a drush_MY_MODULE_post_make() function. But if you need your hook to * be called before drush_make_post_make(), you can ensure this by implemen- * ting MY_MODULE_drush_invoke_alter(). * * @see drush_command_invoke_all_ref() */ function hook_drush_invoke_alter($modules, $hook) { if ($hook == 'some_hook') { // Take the module who's hooks would normally be called last. $module = array_pop($modules); // Ensure it'll be called first for 'some_hook'. array_unshift($modules, $module); } } /** * @} End of "addtogroup hooks". */ # BASH completion script for Drush. # # Place this in your /etc/bash_completion.d/ directory or source it from your # ~/.bash_completion or ~/.bash_profile files. Alternatively, source # examples/example.bashrc instead, as it will automatically find and source # this file. # # If you're using ZSH instead of BASH, add the following to your ~/.zshrc file # and source it. # # autoload bashcompinit # bashcompinit # source /path/to/your/drush.complete.sh # Ensure drush is available. command -v drush >/dev/null || alias drush &> /dev/null || return __drush_ps1() { f="${TMPDIR:-/tmp/}/drush-env-${USER}/drush-drupal-site-$$" if [ -f $f ] then __DRUPAL_SITE=$(cat "$f") else __DRUPAL_SITE="$DRUPAL_SITE" fi # Set DRUSH_PS1_SHOWCOLORHINTS to a non-empty value and define a # __drush_ps1_colorize_alias() function for color hints in your Drush PS1 # prompt. See example.prompt.sh for an example implementation. if [ -n "${__DRUPAL_SITE-}" ] && [ -n "${DRUSH_PS1_SHOWCOLORHINTS-}" ]; then __drush_ps1_colorize_alias fi [[ -n "$__DRUPAL_SITE" ]] && printf "${1:- (%s)}" "$__DRUPAL_SITE" } # Completion function, uses the "drush complete" command to retrieve # completions for a specific command line COMP_WORDS. _drush_completion() { # Set IFS to newline (locally), since we only use newline separators, and # need to retain spaces (or not) after completions. local IFS=$'\n' # The '< /dev/null' is a work around for a bug in php libedit stdin handling. # Note that libedit in place of libreadline in some distributions. See: # https://bugs.launchpad.net/ubuntu/+source/php5/+bug/322214 COMPREPLY=( $(drush --early=includes/complete.inc "${COMP_WORDS[@]}" < /dev/null 2> /dev/null) ) } # Register our completion function. We include common short aliases for Drush. complete -o bashdefault -o default -o nospace -F _drush_completion d dr drush drush5 drush6 drush7 drush8 drush.php drush_version=8.1.13 #!/usr/bin/env sh # # This script is a simple launcher that will run Drush with the most appropriate # php executable it can find. In most cases, the 'drush' script should be # called first; it will in turn launch this script. # # Solaris users: Add /usr/xpg4/bin to the head of your PATH # # Get the absolute path of this executable SELF_DIRNAME="`dirname -- "$0"`" SELF_PATH="`cd -P -- "$SELF_DIRNAME" && pwd -P`/`basename -- "$0"`" # Decide if we are running a Unix shell on Windows if `which uname > /dev/null 2>&1`; then case "`uname -a`" in CYGWIN*) CYGWIN=1 ;; MINGW*) MINGW=1 ;; esac fi # Resolve symlinks - this is the equivalent of "readlink -f", but also works with non-standard OS X readlink. while [ -h "$SELF_PATH" ]; do # 1) cd to directory of the symlink # 2) cd to the directory of where the symlink points # 3) Get the pwd # 4) Append the basename DIR="`dirname -- "$SELF_PATH"`" SYM="`readlink "$SELF_PATH"`" SYM_DIRNAME="`dirname -- "$SYM"`" SELF_PATH="`cd "$DIR" && cd "$SYM_DIRNAME" && pwd`/`basename -- "$SYM"`" done # If not exported, try to determine and export the number of columns. # We do not want to run `tput cols` if $TERM is empty, "unknown", or "dumb", because # if we do, tput will output an undesirable error message to stderr. If # we redirect stderr in any way, e.g. `tput cols 2>/dev/null`, then the # error message is suppressed, but tput cols becomes confused about the # terminal and prints out the default value (80). if [ -z $COLUMNS ] && [ -n "$TERM" ] && [ "$TERM" != dumb ] && [ "$TERM" != unknown ] && [ -n "`which tput`" ] ; then # Note to cygwin/mingw/msys users: install the ncurses package to get tput command. # Note to mingw/msys users: there is no precompiled ncurses package. if COLUMNS="`tput cols`"; then export COLUMNS fi fi if [ -n "$DRUSH_PHP" ] ; then # Use the DRUSH_PHP environment variable if it is available. php="$DRUSH_PHP" else # On MSYSGIT, we need to use "php", not the full path to php if [ -n "$MINGW" ] ; then php="php" else # Default to using the php that we find on the PATH. # We check for a command line (cli) version of php, and if found use that. # Note that we need the full path to php here for Dreamhost, which behaves oddly. See http://drupal.org/node/662926 php="`which php-cli 2>/dev/null`" if [ ! -x "$php" ]; then php="`which php 2>/dev/null`" fi if [ ! -x "$php" ]; then echo "ERROR: can't find php."; exit 1 fi fi fi # Build the path to drush.php. SCRIPT_PATH="`dirname "$SELF_PATH"`/drush.php" if [ -n "$CYGWIN" ] ; then # try to determine if we are running cygwin port php or Windows native php: if [ -n "`"$php" -i | grep -E '^System => Windows'`" ]; then SCRIPT_PATH="`cygpath -w -a -- "$SCRIPT_PATH"`" else SCRIPT_PATH="`cygpath -u -a -- "$SCRIPT_PATH"`" fi fi # Check to see if the user has provided a php.ini file or drush.ini file in any conf dir # Last found wins, so search in reverse priority order for conf_dir in "`dirname "$SELF_PATH"`" /etc/drush "$HOME/.drush" ; do if [ ! -d "$conf_dir" ] ; then continue fi # Handle paths that don't start with a drive letter on MinGW shell. Equivalent to cygpath on Cygwin. if [ -n "$MINGW" ] ; then conf_dir=`sh -c "cd \"$conf_dir\"; pwd -W"` fi if [ -f "$conf_dir/php.ini" ] ; then drush_php_ini="$conf_dir/php.ini" fi if [ -f "$conf_dir/drush.ini" ] ; then drush_php_override="$conf_dir/drush.ini" fi done # If the PHP_INI environment variable is specified, then tell # php to use the php.ini file that it specifies. if [ -n "$PHP_INI" ] ; then drush_php_ini="$PHP_INI" fi # If the DRUSH_INI environment variable is specified, then # extract all ini variable assignments from it and convert # them into php '-d' options. These will override similarly-named # options in the php.ini file if [ -n "$DRUSH_INI" ] ; then drush_php_override="$DRUSH_INI" fi # Add in the php file location and/or the php override variables as appropriate if [ -n "$drush_php_ini" ] ; then php_options="--php-ini $drush_php_ini" fi if [ -n "$drush_php_override" ] ; then php_options=`grep '^[a-z_A-Z0-9.]\+ *=' $drush_php_override | sed -e 's|\([^ =]*\) *= *\(.*\)|\1="\2"|' -e 's| ||g' -e 's|^|-d |' | tr '\n\r' ' '` fi # If the PHP_OPTIONS environment variable is specified, then # its contents will be passed to php on the command line as # additional options to use. if [ -n "$PHP_OPTIONS" ] ; then php_options="$php_options $PHP_OPTIONS" fi # Pass in the path to php so that drush knows which one to use if it # re-launches itself to run subcommands. We will also pass in the php options. # Important note: Any options added here must be removed when Drush processes # a #! (shebang) script. @see drush_adjust_args_if_shebang_script() exec "$php" $php_options "$SCRIPT_PATH" --php="$php" --php-options="$php_options" "$@" #!/usr/bin/env php %|Ӂvs#zIgo-2U]oLM`Tv֏~Ǻ{wv w6|f]@!`q*_<}jNN#qaeG&w3+r[K?C'fжbF m.k * `4Ax^㯂H~` w?hם"d @M*L?̎/U\׍k1fYK"16?74QqLyD|$*!Dݏ͂ h_奀2 o6&*XKiX~F2E0 i0i(K`À `9-Yl@+mNd Ս5ǎ g(51lp6m$fd320/\7'EjZ&Y`MKkJr[( ZO()FaB3p,&G3J̦9 Sa>2 y`d'cl4?pPJ@;@#~YCH“F1n5l8 d&!)TQZI1{%LF0pGXڜeh2i?ev*2Ĉ X`4kڿ.GC1昋2VfR d3h6Q6u IMYZɛ0J7AD1'4`SEPYZB3oi4-5 lR` %`Xڜc|GzȀ irLi0!XEZkh^~$䘌JbBHc'dό{4"zZ*3C}/zghg(h C$~U%U`e>[5"G9 [m<_T#3B˦;z^AWO@+|"B%#CuԑM6\#0l6m,V%vR Oءd9=SVa=>vr蜩/mٌ$tfG|4=0lZ.2![jTYi[DzlUǡ"|)]/=^G7\g)shdjA$N N3[5e!D%'b3aap`C`)6Lݨz)o1*1{쨟?K,ldl)pSèHaqⓓ[g hd4/i?LKfqɀX8"7!sAfH:[|?4,&`'HT,mNA]ďfl?.͟ N O@Jm'yniBtNd@guǼ ` v'r=(˙AcY؏u,6~?}yGSDvOe&{3tѝ{@?'M+#БW?$?if7^͡H8`&tZΙ>/r9))n7 %h}r_U>'ߠ'AK⯑`7URP$%2cl/w,z)"D?>MEЖ& &Q4Yflal#cM s.*L@FL[߹7Ȉ 1pG+4bq2OLǷi["yl` _}*rQ=,kH^b}&N^D/1+[="wDEAR% vv5et,4Ef<_؅Cy|e[=|, < =oVcH@o'i3kIA+ؓfݮ  o2}uEXk)HqLp2 'X|f !HaB2-pN`?mBV4md@3Tl̏3}S9"a;kVdTl0[f䬗?[D@\v wƐD~QzR])l…nk?^yn"086ubt&GAl4DGN1bl:ՍXϓts!hc8woh"y]k 6YrdMcTTECV>lhwn͋ ٢@ W+E6 S],dd̫ ~ Q-O%at"gҬ/0ЈͿ~Qu8f+l@|'_4d?FN clj!É|t:6ϒUMoi}Ur`* c.x!}h<~ 496cՔ شR8y>TDr?رGLKm&F:"$Lbu~Meͪ_+wFÊʁ#LOuNښxA&2F'!3ot2S65]]poS;vv5q˂ I|_9r~<6l/>y ?ASLAV`K;6 㓞F̠2vS>Xe,p`.8|`_ Kx69L簚䬖cGd&t@VdsDQK b1T눩^5 #2\dt{)xqǮEm&5!M6K!DtLI]b=WVS 1ĀKb%A \EvFK$^a2ƖND:,3"0 a`Gt _ODYȎ`U; P<Ȃ~_Zj5i}"PaI|~pqlT<}\8t1X|fT1`>|{n12/86ج q-kL (K:V%!n<mVkRFIU, [V|g.lg T`4zߨa}mȱcp_BZ4;RݶsyV& Bݜv}&jI4VKb `5² F&pfh~4N"̾gw QΨD| &8bDIFYIt1SPrLE1~ *d4O0Z3mcZc_ÃW.WyR]Ɉ8_,>~=͔ 52`WI"'쇍N`l"w rI3\ybPPٛA¥QCWۧ`n0kh` u'NbȎLaB04bdtho;yb5Kh$l}^)J'.IhwU^gEtn9LG T( ~w\ kz|틱`a3'%g "}$|_ 2}6}/M`d6 i=A-!-4 TZ XjYg4I0zdGƍ"+GaߙYICUzxSL[b&^p~+d iIGmV42bAFgJ; `4-(c1":/PɋM~s)d3Uc&TdYYO!n`?-QM(UV@d^vPTF3#5huQJc 4NX̿q^{axH$ [꠲2,5b~zX,f6KA|f4Adh\ <2I"52ְP!e6jm^: [:dr s<LsCNkҥ3Dg0ѐTͦ߮|3FL2)b-xWpLFpML [8VՈ 3k&ƩAKOPNTk`S).w7f!bmZ:xOI2߸B;4\4KNmC=ńV2g4ۺ@ن!w Q@^ᰘLBzRO@l "H 7r[ެ:9Mn9;q4^ҔS˃`4mV>_iuqml߇{ uOQDdcw_\E2x"VVP/AsS@)h..<:ORve Yrql *X G5[J;3`<}o)&';pl18>ʇ1@Ya5!VQieBMg=sq[F^Q/+ϔFV4߫@M=4Q-:4יs_[bE=8<#%c2N74 8qNOx_Y;֗u.wb7RcHkڑi_C_QKN ttV1ϛ4r6kl \嗪'4c&MMi2ΈH IvvaA`  =dV}ԣ#>@a `O.` $X1Sb9;"h:&x>oyh @܈;0>\ 5F|}]ml?[S p#vP7*lm>"tԙ `th1Y[PR H)#| @7)3/^Ҿ Z,>wL ƓY{rpU &Pto*u96tΈ"1<3DN Ir&_!Ҋ}. Tƒi;d 9Mȓpl$:^MeaB)7ԡ6ќ0% 6'4:ѾQʫ7DQ _ItL`ȉ&V:MGlX{6u:e(lX&N v|ظt&yqPlN|5"TW(_ɮv/65#TNB%5uNҧQ`EK~=`@ QZSQ,ۯz !zd~Nm^-v=`/"; 9bԼitqTE$ԻgMD]`0nmMl1S35Gh*ݿ22#ńBB*q'ؘђQ;yy)LgyCH(5uy_LGV@-H 0B<|hqy~nM JpZOlB$N^U ݂O?)XQ r4jC($TxPDTin?GTd ! Lb WMGTaL`&އMF=5me`ۆ֙Eh]P~d}&<~0e@Б9;א3 [zS+9⢧dYR48*|ig"h ٸeM! ")5lKx}u۵u1m#sUlz>|7nᚍ&In!5. A҉e? d>O9c>zxjIj>trbҗO{GI]Uf (jN 5`_F s-js@HX #nr1g7asrQL@YݎYZɈ"%TMn hziaU<+%g댙IjjL@Yi@EF l,'b3hB'$#Iz ^//B>x!)lb eqݐ}6T ;38ӋԠL9("Ct:"/Q?'l_~X )"Y= tP m=˛ /D;+SWDVr9f ӵq5籅KJ@K54\F&nI¹28_=Ƈiz"*,|R_tUxl8wo*V+ FfbSwhvQ5c{W``P)}~ACX[: Dtm24t.eR:fG s#@u9BV 6vA:fm"ݓ>d>3Bޞ>mT jy'7TD6y|#>rtr6j&.v;ZDGq83NY3)24t>AΥ ,BEoJA><"fn/b#K9#9#*Q_&cHGR3y,Xr}_ ̫6Tھ# qWkFʓ˻'4kU/H TTLtF@a@JGX Fohq +IE?j#*g( 29IcVY=}A:(EFC`Ҙk*OFOz> .COƱϼ_VC+:rQ:DIϗ!V(c;6v 7rr`ōJ5THÈ$g:s_yv?]@\v|"GT(oF-Lو@hҬZPvoJu/^9t%"a41>> / "=<(:gF55T N%˦nSM8G^QAKrV afkx@-QdVvϟ7k6}в,i{K?#. hԹzS3F܈_Vೱuݟ( @f p$](T>*7bӱPdXmVrk+w<]Aʨ3tV:'*p!nADKߨ=f ;7gw^ 6zn텞^*YV"8%橑ֻ>N6]*.)*lNEL\"4q(| L_SHóDE]f~X4+K簴eu`qL*覚Ó^~0A&7bfSy.(86ikN4Z5WհݺFf{iɻRL")4|?F"c.()xIB|^٨Ϩhq[{V|ɉLXPeuO~vJDA@#5Zɷ5vVvȵ;T`3M50aUyXd҅xF> d52z?2lU!&*3c1ܓM$rY u{mDUAwPVSlIeG&4FJA<_e<Ȑ,-xS ^Ak *nR{Ԡ hT(аx㡠y {DT7cmJm1rIs(P<5e{ &kRVBbqU`|Li2"OH)S2Əs|b>jR  ,@zu ]RA,CJDb3 l6#QɠQL3IdE–]2V S1 }d~z-x[f2y2ʤ~e͌pbapuJ@Hl[ Y%v.ifE0h6F+9S-S9$x`|W[)db xi2R]2f~/"7Y_AdM-=z&9ySt`'L2FOku-kFjoff(݇H?$ҁ.3 s9SE6#NQ:tDLS 5!7sQ0,S@ìkp~4i"{Z^G͗&siTΊ@ àac+ z&ZlNs밞[~ ,oH#HOC #B } b7hSQCfb=LG 9J!3Cω7G~խe3_>Lҗ)8%d [MdS!!\0Re/P0Tڋf:"G'v5u0#pp` ԐiБmUs,-!`GU\8rь8 xvi-29ZmxC2H@^e2*A&\LKɹ*acڽb;jOAȦ's̠p/[BJ̆AkZau1ƱזU‚8,K[<>=#poGbu3̾h*G7AMLoF慞3l@¤bY?iHD2\-9vn̐,3RtЁԋ 2qr\7Iʺ'2EXb=blD11sL!|N 4r}`F@EF$56"瓍DE[a7?LFf@c LU0g@&fH[u+#㍤Xˑ>k8ˆA㭋[ ^޻Z@d0LPr'b-ۓ_f-:ӁUj@dz](CH/7yS,e2MFwM#zZӟbAD}Ii'` u[]l^ym@$[tf;~F!L"}s]|h* \CWC.YRR8"F8naXHZ"IMJiC .$5)M6ȄΎ6,Pe b{Dq1*1~ mrʜdؐ ti>ZJMGqc|AX+SMrDd-N 星p+X6&i1IlTSQQBZ *c2b X˴g8Y9on4rR5ϕ3(r`” bX1%l ||e/; HaEdEe|$  `9⧢pa(& E1 LIpӜ &ZHpHTT Оʏɐ`n2\M׵H7S]UdV sZ +X91p܎7G-9*&_0"IhE;A$tgLD">]R{Vuyei" q7 N 9iL:NƱc*J$t o> s3|ة0n.~UdbH:H8׬ J 3V q,#: -wQc-Z巙9kHnRsdB4U4Og= H\FY4yLG$lVp87ÜNbuYǔBXoMUJ6}&V6I pxsL4p?& |S[Hse>7(cop1 H)L|-/A9}58)H=?tGg-X`- 8&!SnOEz\c844yDQo?uac*SzjaZ2"_l&\;!(JA/ށw-m1sʝ4)6*oD`l _W )b{!}w0$YTU5I~tRqAXfK4j\$}/|+J*x\WCŒ~Nb0rUD*1&wU,'ʇ4}ULGțLDwf1RWUR&# 1Q8-1/)&3w׈\7İB'4s"9T`7l*?w?h*Q8MY0Aw  2W̍@yȇ/4Xj-Iu4w!O cr Ɍi;Q!.~?iJ#V2C qE---F_~ٸݱc8q^2=鹶&>dRл/s%{e0ZrfNoQrD+ KaWH\h['php̈'@usR#f\4!3%-vcIs |"ДAekfk-⋙[÷ƀ{'8 2"T1.\T.J{$oyZb$Fx,a3P6w0+^cMG@ M/oF͎"iSQIp.>yEdGzx/0V!cFvO>*LA&(Q H [[[|'TWU/߻w/<0<<T!.2Ī!4%3 }ZP4U/Ț(,Em` 9Y\ $fRZd(`b |F>yp{/p ࡪAˤbW\+~W~tӈ*ZJߌR \(Ra ." m~0&> g54-iUM4iFG|P2yjjlܢ<53KXr \39FFFG?܃ZE}!stJqehٞnY 0~}nVQӂ9pE pM@ͤ 1}.^xGа`Tp*jX;(`퐵w OScj| /Q^WRNV|CA5BS:T2<=}mf0_9>e=@'> Ū ~kVٰ6nU0E`8%`tոZ$yc'9Nnk T%Z 6jaJrDɀGJa/~K 74K.u6o(I">\EugSO\ &[mt1\=G;1:a\'r&a [I_g$sgYE ;c4?s~Io|d2kTUUwغu+܊'>b7sl4|mFH:19hmvCj;ONŤfڌtra Wvjv MF^WD؝a@^Q!.ky5o/Pꣳ.RxCa"|2!:i?!z{|}kRF(k8H9k=G'?4,b1:6{_#10򢆗H"jD&;j2}1 ˿ȗDJp3]vA* 22&b8ۿ+,]tZO,~z{{a-6ມ}xKN6>B.rɺ <itG&Pln1s\C`x>*6*cZZ\/DT0DG `&#\G;7ȐYa; al]ZVplcq3 ۾tXW,1rQ@6PW @M0h5I x# *wyĿx^tۈ_FF͵ុ??" kgbi1eI*yvƟ‰.lN /=kR*pR[7jL$\J_w0da۷d2<|-?wMBUU Te4zdnKw\y+al uf x TFyxkqƳ5+!c<΋\ \60- #| {/f7va2~ye1X&%hww]їWmRPGaXrr ;A }U@GlhDP!f6~}71u>`[;dBH]]OB1!kR]^}*Ayl$w˯~=<6{]~ށ56>t/%1+Keb.V@L8Dш8eNY-p. cVcLqI/[gYim;ұl(N N2(-L+*zNkuŗ@yEn ID |HV.Օ00xR$6nm eQOp-_C2Ji9r*[?D"<1 1غ؞9m0淴 5K`a b/DkKUU22XI0Zh a 5 6f9l MP|>,25M,bAtO~QX{׭oZ[g8wg% (R3-V3,Oa_/eEQnjQQ[80wbAFGp7B,'>t*̱ɠ!")(d^b\VKHXn!ty# |:x`fL1<27_)x𡇬`,sh$crL:~rl>&U?-B=*\@W_|mlf%Xm6K}cq=0<䔝+%`Lrt3"!-Յ.5];cmǏ?+@F?#ۜ;OdIӏ|pر;Y 5JFlYjɬϟږT4|l4x2`#lFj:iMXE 6b1Z-ЭY%֪9d.i@Y3'?h&JTE$ ">HBdCf5 d/5f54Wө-D(\;>SaS 9ҐW;f>ev`/F2)m ?!p]͎=bVQ BEhfKFI|t!|~m<9PH?˰'ū>!}PRE},H{$}{=%[l޼^zbW\n3{cza`8OpQ,!b|;h~jWج}*9AX \e˗wnRktڈQg4MަU,dxI$Dy~_JdD'FT$6Eˉ$]]pwpWłDl?ۚ5SnfdшX,V~|xSc-\;^َ$6 l<K˃ټon|{J# ;&S)M¦4oTU] )@wVC|)[h6lK h '7'aYuhK670ñkn?Z H(F*@x猨822,o̱;,BaӦ&qw }$l+[g>H&'8ī"BV6.8oe9@[o%uAb~E[82@v5T0"_ /9 `dk1+QlJ}u]\ !5H:[ j MM BeTm3۩Md9oO| ~|#? N}tI ',ъuBZ?gkƈv"D a 7ɉ kfПLK0Q!j!l8"Z +<4V3'СC+v22bV"2I<%84;v$?w}wrO%x1xbp/&XM5{ٻx %e$6#7ob,8_z%F|xꩧfn7DHIOfߞm`~rXѐӝGIRtؘ\fmpʾ>"ic`ʕF|Do2o04}RO o|l}-|~ a].\ .!'@cP> <!Һ|-²Sږsh/㛎+cE`/fIWoe$< ${ *WSS.ʓ$D&~zHIYBP9Ŀ%0e_ b6׽ʐ3>AT8:T iViy/d~īyEdB%& #}k6Õ ;SCg@<  /FFd" (>MEMdϏ))c)%fEIY b/YYb pE\kns<*ɅD8lB`>l7dWU> Cg@M"̑u#z9㣙07-%uR`.m8;C4 jbΡd`@ˎϘdDn^If">Y+#"Yh&Gt9ٓ P1_Rh C~s?9>D‚޸<ڌDk%4b[ސ^Cǡ|=$00`%e *cŭs2Ħ%eɥk3Q18 `^ca%vY[,CԸBh(hzőr!嗽D0J j3Qb=h,'L(\uLСlA׺gpAVcɵ7ׂM>0*oYO?YIy4w@E}}RB%υbB(\(N14b7I$XD2mZUL@dfֹ Y"m.oT$ z_p6 Oq{ܘ|.bR\9>J1Ŀ4P5T1[Ydf#99T@6*Ͽ7u@lve2޴*8 mO?-Sj@,ѰЫկ5F͗q$L2P"&8aXLR19R+E(o 7fNfzK6qsʼ~Q>O#-d#?D>;߁Wwo>, +]3hsYW] c .PIgJ`0fL'gғ12x*Vlmf( RMkڦďl6bI ump *W0J| jE'ٳHnzŐ`-Ʒm6\ "+@6cur>fuR6#&#חl$t| *iP $ٟ LٔrUd9-BB._+{Dw43& >uI;Tm2(ŝ;&cf[r ݚc"%Q~xLx2ЙnO3*#rv|{SQ3~$|N1Y&QDHn,TR`@  fFvZުXΌ`BFR4|T2Sgᚷ\n021 *:h%;`*ɦk01zȠ( -lfZմfаQsWCCE~`0dF~#kND̈́$=hkm뮻ok׮-!2C ڽXݏj*Vi8ӳ3MZqPQAb:vDь7Hgzc@2YHf}!mq|Ѝ6sȤ& GH>Ehٻgn[$!uo򻜋L=/f'%jAeMUT5AۊdD inU*닕=݌F"=Mjà*Pw oFs tag}yΕx?K4^83PMkNZnc1#DU Iz8z=}}FG-/UMUygx@_h2& =KH uM]G g3"Ep k_,Y_&b+Rfi.mUbp0Ϭପotr R|)P?o)K[Ӯ: 6b2ڍ*#6'B.sZ,-?Wi%s}-(UJ1.Kʴ>Ka*Λ `L ԴjkaddPa!Qb䓍I-ln;hN-&}ofE.5zf̆y g|L,#/qcVib+Bn,fpEV`;yzVnmF' _`e:"dtt k?`c2fR@ЈVd,`PH,Ez` ~$'Im?dRKRi̙ We1h.Y锃eK/Wv#> )S.3Z쩮ܟQy6_޾]j2N ou4|q&jB8p B\ =3A>,!H!*m~函0za&|'M0ba:TG EnZHԴ@rr$/v#Gvn?5Cd`[\ u9tɘ9m$?CӼ E!ZA" ȵqP|rf"b0"ŨM$3Krʔz&`GLMMcQ_FR\aEP״Dj5zf/5+`aM6}^M0yk^Yכi|?-Pl^ჽT!2 #ΕFnɔ̇yIeutj`8j`Ғ&H&gAR۔e^rnV:`~ ӯNU$l+i*!^[zزQp.Uk|L <zVB+V^ ln0Ag-N@GC4G؎?HރF}k;To)Z~[!2.^@rW*Y8CnTw$NcF2.*MPfJMMyX{ū&b8mlKsBruP3;_z7(LjDofbD lh2%!5tP>"dŐIa^}GJg ڎH +8"7 P,/X14\y!eeQ}І{ &Ȉ%Mo?\GzI)`?(Qkf*#Whp;Lndop92ii*sPZJC&BӢKE_zޑ[q^;:~[5i?$~<"e8 +ݸu+yMIexeLrX!_S7<%m5M7fH >eb~Ĭt,)"ǓmR 6u K;υPABnWj-ݺ IFOO.wC4/kZ^&sel̓'Ig6 ҵ>mZ| _ɞ*OCj2"EGϴ@fr(e|t+\;52mwn 7Sw`hC؆3RAm6AV@Lj O'Ge)[m hAH4p5L󑰉sy;΃e,ƫpTy;p^|.w'ksQ%zlmVAV@ }y髙7!U[eD)"7MM+Q='OL-cGat(AOҝHKڦ7ͺ0Źg˼GO1 Tu .{ 1Av0|.@-w*MSCHTr-[e?pMCu̲VlDUy,(g;D?NVJMBE 1!yW2e`;LC0bL |iy*!F[tRH:ɤ I,F@+Ty$R$,@YxQ{!joUߡ<@N>51nO#Lf%GKo-hkKuQъ0Bg2%vX^E,7k>4+k50PgˌVD UeH\.f.وīBHhszL BKHwy}l,mQC4ܾ %K!(?NRY0 EDܾ:Qkz GRYj"(:<,vD(&Klϟu;DOSD2:YuQH"x詜)uS!>Q-Isne_8#T>eJZ5!)I߽t,cߊFCZ9h{|2Њ4)( ]7?܅ͨg&cc(eӱ< Q: What does "drush" stand for?
    > A: The Drupal Shell. > > Q: How do I pronounce Drush?
    > A: Some people pronounce the *dru* with a long 'u' like Dr*u*pal. Fidelity points > go to them, but they are in the minority. Most pronounce Drush so that it > rhymes with hush, rush, flush, etc. This is the preferred pronunciation. > > Q: Does Drush have unit tests?
    > A: Drush has an excellent suite of unit tests. See > [tests/README.md](https://github.com/drush-ops/drush/blob/master/tests/README.md) for more information. Credits ----------- * Originally developed by [Arto Bendiken](http://bendiken.net) for Drupal 4.7. * Redesigned by [Franz Heinzmann](http://unbiskant.org) in May 2007 for Drupal 5. * Maintained by [Moshe Weitzman](http://drupal.org/moshe) with much help from the folks listed at https://github.com/orgs/drush-ops/people. ![Drush Logo](drush_logo-black.png) "drush wrapper" -> "drush launcher". * * Brief description of each: * * - Drush finder: Finds the right Drush script and calls it. * - Drush wrapper: Contains user customizations to options. * - Drush launcher: Excutes drush.php. * * A full explanation of each script follows. * * * DRUSH FINDER * * - The "drush" script on the user's global $PATH * - It's goal is to find the correct site-local Drush to run. * * The Drush finder will locate the correct site-local Drush to use * by examining: * * a) The --root option * b) The site set via `drush site set` in the current terminal * c) The cwd * * If no site-local Drush is found, then the global Drush will be * used. The Drush finder assumes that the global Drush is the * "Drush launcher" found in the same directory as the Drush finder itself. * * If a site-local Drush is found, then the Drush finder will call * either the "Drush wrapper", if it exists, or the "Drush launcher" if * there is no wrapper script. * * * DRUSH WRAPPER * * - The drush.wrapper script that the user optionally copies and edits. * - Its goal is to allow the user to add options when --local is in use * * The Drush "wrapper" is found at examples/drush.wrapper, and may optionally * be copied to __ROOT__ by the user. It may be named either * "drush" or "drush.wrapper". It will call the "Drush launcher" * for the same site that it is located in. It adds the --local flag; the * user is encouraged to add other options to the "Drush wrapper", e.g. to set * the location where aliases and global commandfiles can be found. * The Drush "finder" script always calls the "Drush wrapper" if it exists; * however, if the user does not want to customize the early options of * the site-local Drush (site-alias locations, etc.), then the wrapper does not * need to be used. * * * DRUSH LAUNCHER * * - The "drush.launcher" script in vendor/bin * - The bash script formerly called "drush" * * The "Drush launcher" is the traditional script that identifies PHP and * sets up to call drush.php. It is called by the "Drush wrapper", or * directly by the "Drush launcher" if there is no "Drush wrapper" in use. * * * LOCATIONS FOR THESE SCRIPTS * * "Drush finder" : __ROOT__/vendor/bin/drush (composer install) * __DRUSH__/drush (source) * * "Drush wrapper" : __ROOT__/drush (copied by user) * __DRUSH__/examples/drush.wrapper (source) * * "Drush launcher" : __ROOT__/vendor/bin/drush.launcher (composer install) * __DRUSH__/drush.launcher (source) * * * BACKEND CALL DISPATCHING * * Backend calls are typically set up to call the "drush" script in the $PATH, * or perhaps some might call __ROOT__/vendor/bin/drush directly, by way * of the "drush-script" element in a site alias. In either event, this is * the "drush finder" script. * * The backend call will always set --root. The "Drush finder" script * always favors the site-local Drush stored with the site indicated by the * --root option, if it exists. If there is no site-local Drush, then the * "Drush finder" will behave as usual (i.e., it will end up calling the * "Drush launcher" located next to it). * * This should always get you the correct "Drush" for local and remote calls. * Note that it is also okay for aliases to specify a path directly to * drush.launcher, in instances where it is known that a recent version of * Drush is installed on the remote end. */ if (!function_exists("drush_startup")) { include __DIR__ . '/includes/startup.inc'; } drush_startup($argv); ˁn(_qaenE=_GBMB