diff --git a/application/class/Url.php b/application/class/Url.php index bb5f451..d196156 100644 --- a/application/class/Url.php +++ b/application/class/Url.php @@ -243,4 +243,18 @@ class Url } return $url . BASE_SERVER_DIRECTORY; } + /** + * Obtiens le fragment depuis une variable serveur, + * ce qui est selon moi possible avec une bonne configuration serveur + * sauf si le fragment est bloqué au niveau du navigateur (rétention d'informations) + * + * Selon QASTACK + * => https://qastack.fr/programming/2317508/get-fragment-value-after-hash-from-a-url-in-php + * ce n'est pas possible avec HTTP "standard" car cette valeur n'est jamais envoyée au serveur + * (par conséquent, elle ne sera pas disponible dans $_SERVER["REQUEST_URI"]ou similaire variables prédéfinies) + */ + public static function getFragment(){ + $fragment = parse_url($_SERVER['REQUEST_URI'],PHP_URL_FRAGMENT); + return $fragment; + } } diff --git a/build/bin/phpDocumentor.phar b/build/bin/phpDocumentor.phar new file mode 100644 index 0000000..0be8823 Binary files /dev/null and b/build/bin/phpDocumentor.phar differ diff --git a/build/bin/phpcpd-6.0.3.phar b/build/bin/phpcpd-6.0.3.phar new file mode 100644 index 0000000..4ae9e33 Binary files /dev/null and b/build/bin/phpcpd-6.0.3.phar differ diff --git a/build/bin/phpdcd-1.0.2.phar b/build/bin/phpdcd-1.0.2.phar new file mode 100644 index 0000000..bfd16fb --- /dev/null +++ b/build/bin/phpdcd-1.0.2.phar @@ -0,0 +1,20310 @@ +#!/usr/bin/env php + '/php-timer/Timer.php', + 'php_token' => '/php-token-stream/Token.php', + 'php_token_abstract' => '/php-token-stream/Token.php', + 'php_token_ampersand' => '/php-token-stream/Token.php', + 'php_token_and_equal' => '/php-token-stream/Token.php', + 'php_token_array' => '/php-token-stream/Token.php', + 'php_token_array_cast' => '/php-token-stream/Token.php', + 'php_token_as' => '/php-token-stream/Token.php', + 'php_token_at' => '/php-token-stream/Token.php', + 'php_token_backtick' => '/php-token-stream/Token.php', + 'php_token_bad_character' => '/php-token-stream/Token.php', + 'php_token_bool_cast' => '/php-token-stream/Token.php', + 'php_token_boolean_and' => '/php-token-stream/Token.php', + 'php_token_boolean_or' => '/php-token-stream/Token.php', + 'php_token_break' => '/php-token-stream/Token.php', + 'php_token_callable' => '/php-token-stream/Token.php', + 'php_token_caret' => '/php-token-stream/Token.php', + 'php_token_case' => '/php-token-stream/Token.php', + 'php_token_catch' => '/php-token-stream/Token.php', + 'php_token_character' => '/php-token-stream/Token.php', + 'php_token_class' => '/php-token-stream/Token.php', + 'php_token_class_c' => '/php-token-stream/Token.php', + 'php_token_class_name_constant' => '/php-token-stream/Token.php', + 'php_token_clone' => '/php-token-stream/Token.php', + 'php_token_close_bracket' => '/php-token-stream/Token.php', + 'php_token_close_curly' => '/php-token-stream/Token.php', + 'php_token_close_square' => '/php-token-stream/Token.php', + 'php_token_close_tag' => '/php-token-stream/Token.php', + 'php_token_colon' => '/php-token-stream/Token.php', + 'php_token_comma' => '/php-token-stream/Token.php', + 'php_token_comment' => '/php-token-stream/Token.php', + 'php_token_concat_equal' => '/php-token-stream/Token.php', + 'php_token_const' => '/php-token-stream/Token.php', + 'php_token_constant_encapsed_string' => '/php-token-stream/Token.php', + 'php_token_continue' => '/php-token-stream/Token.php', + 'php_token_curly_open' => '/php-token-stream/Token.php', + 'php_token_dec' => '/php-token-stream/Token.php', + 'php_token_declare' => '/php-token-stream/Token.php', + 'php_token_default' => '/php-token-stream/Token.php', + 'php_token_dir' => '/php-token-stream/Token.php', + 'php_token_div' => '/php-token-stream/Token.php', + 'php_token_div_equal' => '/php-token-stream/Token.php', + 'php_token_dnumber' => '/php-token-stream/Token.php', + 'php_token_do' => '/php-token-stream/Token.php', + 'php_token_doc_comment' => '/php-token-stream/Token.php', + 'php_token_dollar' => '/php-token-stream/Token.php', + 'php_token_dollar_open_curly_braces' => '/php-token-stream/Token.php', + 'php_token_dot' => '/php-token-stream/Token.php', + 'php_token_double_arrow' => '/php-token-stream/Token.php', + 'php_token_double_cast' => '/php-token-stream/Token.php', + 'php_token_double_colon' => '/php-token-stream/Token.php', + 'php_token_double_quotes' => '/php-token-stream/Token.php', + 'php_token_echo' => '/php-token-stream/Token.php', + 'php_token_else' => '/php-token-stream/Token.php', + 'php_token_elseif' => '/php-token-stream/Token.php', + 'php_token_empty' => '/php-token-stream/Token.php', + 'php_token_encapsed_and_whitespace' => '/php-token-stream/Token.php', + 'php_token_end_heredoc' => '/php-token-stream/Token.php', + 'php_token_enddeclare' => '/php-token-stream/Token.php', + 'php_token_endfor' => '/php-token-stream/Token.php', + 'php_token_endforeach' => '/php-token-stream/Token.php', + 'php_token_endif' => '/php-token-stream/Token.php', + 'php_token_endswitch' => '/php-token-stream/Token.php', + 'php_token_endwhile' => '/php-token-stream/Token.php', + 'php_token_equal' => '/php-token-stream/Token.php', + 'php_token_eval' => '/php-token-stream/Token.php', + 'php_token_exclamation_mark' => '/php-token-stream/Token.php', + 'php_token_exit' => '/php-token-stream/Token.php', + 'php_token_extends' => '/php-token-stream/Token.php', + 'php_token_file' => '/php-token-stream/Token.php', + 'php_token_final' => '/php-token-stream/Token.php', + 'php_token_finally' => '/php-token-stream/Token.php', + 'php_token_for' => '/php-token-stream/Token.php', + 'php_token_foreach' => '/php-token-stream/Token.php', + 'php_token_func_c' => '/php-token-stream/Token.php', + 'php_token_function' => '/php-token-stream/Token.php', + 'php_token_global' => '/php-token-stream/Token.php', + 'php_token_goto' => '/php-token-stream/Token.php', + 'php_token_gt' => '/php-token-stream/Token.php', + 'php_token_halt_compiler' => '/php-token-stream/Token.php', + 'php_token_if' => '/php-token-stream/Token.php', + 'php_token_implements' => '/php-token-stream/Token.php', + 'php_token_inc' => '/php-token-stream/Token.php', + 'php_token_include' => '/php-token-stream/Token.php', + 'php_token_include_once' => '/php-token-stream/Token.php', + 'php_token_includes' => '/php-token-stream/Token.php', + 'php_token_inline_html' => '/php-token-stream/Token.php', + 'php_token_instanceof' => '/php-token-stream/Token.php', + 'php_token_insteadof' => '/php-token-stream/Token.php', + 'php_token_int_cast' => '/php-token-stream/Token.php', + 'php_token_interface' => '/php-token-stream/Token.php', + 'php_token_is_equal' => '/php-token-stream/Token.php', + 'php_token_is_greater_or_equal' => '/php-token-stream/Token.php', + 'php_token_is_identical' => '/php-token-stream/Token.php', + 'php_token_is_not_equal' => '/php-token-stream/Token.php', + 'php_token_is_not_identical' => '/php-token-stream/Token.php', + 'php_token_is_smaller_or_equal' => '/php-token-stream/Token.php', + 'php_token_isset' => '/php-token-stream/Token.php', + 'php_token_line' => '/php-token-stream/Token.php', + 'php_token_list' => '/php-token-stream/Token.php', + 'php_token_lnumber' => '/php-token-stream/Token.php', + 'php_token_logical_and' => '/php-token-stream/Token.php', + 'php_token_logical_or' => '/php-token-stream/Token.php', + 'php_token_logical_xor' => '/php-token-stream/Token.php', + 'php_token_lt' => '/php-token-stream/Token.php', + 'php_token_method_c' => '/php-token-stream/Token.php', + 'php_token_minus' => '/php-token-stream/Token.php', + 'php_token_minus_equal' => '/php-token-stream/Token.php', + 'php_token_mod_equal' => '/php-token-stream/Token.php', + 'php_token_mul_equal' => '/php-token-stream/Token.php', + 'php_token_mult' => '/php-token-stream/Token.php', + 'php_token_namespace' => '/php-token-stream/Token.php', + 'php_token_new' => '/php-token-stream/Token.php', + 'php_token_ns_c' => '/php-token-stream/Token.php', + 'php_token_ns_separator' => '/php-token-stream/Token.php', + 'php_token_num_string' => '/php-token-stream/Token.php', + 'php_token_object_cast' => '/php-token-stream/Token.php', + 'php_token_object_operator' => '/php-token-stream/Token.php', + 'php_token_open_bracket' => '/php-token-stream/Token.php', + 'php_token_open_curly' => '/php-token-stream/Token.php', + 'php_token_open_square' => '/php-token-stream/Token.php', + 'php_token_open_tag' => '/php-token-stream/Token.php', + 'php_token_open_tag_with_echo' => '/php-token-stream/Token.php', + 'php_token_or_equal' => '/php-token-stream/Token.php', + 'php_token_paamayim_nekudotayim' => '/php-token-stream/Token.php', + 'php_token_percent' => '/php-token-stream/Token.php', + 'php_token_pipe' => '/php-token-stream/Token.php', + 'php_token_plus' => '/php-token-stream/Token.php', + 'php_token_plus_equal' => '/php-token-stream/Token.php', + 'php_token_print' => '/php-token-stream/Token.php', + 'php_token_private' => '/php-token-stream/Token.php', + 'php_token_protected' => '/php-token-stream/Token.php', + 'php_token_public' => '/php-token-stream/Token.php', + 'php_token_question_mark' => '/php-token-stream/Token.php', + 'php_token_require' => '/php-token-stream/Token.php', + 'php_token_require_once' => '/php-token-stream/Token.php', + 'php_token_return' => '/php-token-stream/Token.php', + 'php_token_semicolon' => '/php-token-stream/Token.php', + 'php_token_sl' => '/php-token-stream/Token.php', + 'php_token_sl_equal' => '/php-token-stream/Token.php', + 'php_token_sr' => '/php-token-stream/Token.php', + 'php_token_sr_equal' => '/php-token-stream/Token.php', + 'php_token_start_heredoc' => '/php-token-stream/Token.php', + 'php_token_static' => '/php-token-stream/Token.php', + 'php_token_stream' => '/php-token-stream/Token/Stream.php', + 'php_token_stream_cachingfactory' => '/php-token-stream/Token/Stream/CachingFactory.php', + 'php_token_string' => '/php-token-stream/Token.php', + 'php_token_string_cast' => '/php-token-stream/Token.php', + 'php_token_string_varname' => '/php-token-stream/Token.php', + 'php_token_switch' => '/php-token-stream/Token.php', + 'php_token_throw' => '/php-token-stream/Token.php', + 'php_token_tilde' => '/php-token-stream/Token.php', + 'php_token_trait' => '/php-token-stream/Token.php', + 'php_token_trait_c' => '/php-token-stream/Token.php', + 'php_token_try' => '/php-token-stream/Token.php', + 'php_token_unset' => '/php-token-stream/Token.php', + 'php_token_unset_cast' => '/php-token-stream/Token.php', + 'php_token_use' => '/php-token-stream/Token.php', + 'php_token_var' => '/php-token-stream/Token.php', + 'php_token_variable' => '/php-token-stream/Token.php', + 'php_token_while' => '/php-token-stream/Token.php', + 'php_token_whitespace' => '/php-token-stream/Token.php', + 'php_token_xor_equal' => '/php-token-stream/Token.php', + 'php_token_yield' => '/php-token-stream/Token.php', + 'php_tokenwithscope' => '/php-token-stream/Token.php', + 'php_tokenwithscopeandvisibility' => '/php-token-stream/Token.php', + 'sebastianbergmann\\finderfacade\\configuration' => '/finder-facade/Configuration.php', + 'sebastianbergmann\\finderfacade\\finderfacade' => '/finder-facade/FinderFacade.php', + 'sebastianbergmann\\phpdcd\\analyser' => '/src/Analyser.php', + 'sebastianbergmann\\phpdcd\\cli\\application' => '/src/CLI/Application.php', + 'sebastianbergmann\\phpdcd\\cli\\command' => '/src/CLI/Command.php', + 'sebastianbergmann\\phpdcd\\detector' => '/src/Detector.php', + 'sebastianbergmann\\phpdcd\\log\\text' => '/src/Log/Text.php', + 'sebastianbergmann\\version' => '/version/Version.php', + 'symfony\\component\\console\\application' => '/symfony/console/Symfony/Component/Console/Application.php', + 'symfony\\component\\console\\command\\command' => '/symfony/console/Symfony/Component/Console/Command/Command.php', + 'symfony\\component\\console\\command\\helpcommand' => '/symfony/console/Symfony/Component/Console/Command/HelpCommand.php', + 'symfony\\component\\console\\command\\listcommand' => '/symfony/console/Symfony/Component/Console/Command/ListCommand.php', + 'symfony\\component\\console\\consoleevents' => '/symfony/console/Symfony/Component/Console/ConsoleEvents.php', + 'symfony\\component\\console\\descriptor\\applicationdescription' => '/symfony/console/Symfony/Component/Console/Descriptor/ApplicationDescription.php', + 'symfony\\component\\console\\descriptor\\descriptor' => '/symfony/console/Symfony/Component/Console/Descriptor/Descriptor.php', + 'symfony\\component\\console\\descriptor\\descriptorinterface' => '/symfony/console/Symfony/Component/Console/Descriptor/DescriptorInterface.php', + 'symfony\\component\\console\\descriptor\\jsondescriptor' => '/symfony/console/Symfony/Component/Console/Descriptor/JsonDescriptor.php', + 'symfony\\component\\console\\descriptor\\markdowndescriptor' => '/symfony/console/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php', + 'symfony\\component\\console\\descriptor\\textdescriptor' => '/symfony/console/Symfony/Component/Console/Descriptor/TextDescriptor.php', + 'symfony\\component\\console\\descriptor\\xmldescriptor' => '/symfony/console/Symfony/Component/Console/Descriptor/XmlDescriptor.php', + 'symfony\\component\\console\\event\\consolecommandevent' => '/symfony/console/Symfony/Component/Console/Event/ConsoleCommandEvent.php', + 'symfony\\component\\console\\event\\consoleevent' => '/symfony/console/Symfony/Component/Console/Event/ConsoleEvent.php', + 'symfony\\component\\console\\event\\consoleexceptionevent' => '/symfony/console/Symfony/Component/Console/Event/ConsoleExceptionEvent.php', + 'symfony\\component\\console\\event\\consoleterminateevent' => '/symfony/console/Symfony/Component/Console/Event/ConsoleTerminateEvent.php', + 'symfony\\component\\console\\formatter\\outputformatter' => '/symfony/console/Symfony/Component/Console/Formatter/OutputFormatter.php', + 'symfony\\component\\console\\formatter\\outputformatterinterface' => '/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterInterface.php', + 'symfony\\component\\console\\formatter\\outputformatterstyle' => '/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyle.php', + 'symfony\\component\\console\\formatter\\outputformatterstyleinterface' => '/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleInterface.php', + 'symfony\\component\\console\\formatter\\outputformatterstylestack' => '/symfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php', + 'symfony\\component\\console\\helper\\descriptorhelper' => '/symfony/console/Symfony/Component/Console/Helper/DescriptorHelper.php', + 'symfony\\component\\console\\helper\\dialoghelper' => '/symfony/console/Symfony/Component/Console/Helper/DialogHelper.php', + 'symfony\\component\\console\\helper\\formatterhelper' => '/symfony/console/Symfony/Component/Console/Helper/FormatterHelper.php', + 'symfony\\component\\console\\helper\\helper' => '/symfony/console/Symfony/Component/Console/Helper/Helper.php', + 'symfony\\component\\console\\helper\\helperinterface' => '/symfony/console/Symfony/Component/Console/Helper/HelperInterface.php', + 'symfony\\component\\console\\helper\\helperset' => '/symfony/console/Symfony/Component/Console/Helper/HelperSet.php', + 'symfony\\component\\console\\helper\\inputawarehelper' => '/symfony/console/Symfony/Component/Console/Helper/InputAwareHelper.php', + 'symfony\\component\\console\\helper\\progresshelper' => '/symfony/console/Symfony/Component/Console/Helper/ProgressHelper.php', + 'symfony\\component\\console\\helper\\tablehelper' => '/symfony/console/Symfony/Component/Console/Helper/TableHelper.php', + 'symfony\\component\\console\\input\\argvinput' => '/symfony/console/Symfony/Component/Console/Input/ArgvInput.php', + 'symfony\\component\\console\\input\\arrayinput' => '/symfony/console/Symfony/Component/Console/Input/ArrayInput.php', + 'symfony\\component\\console\\input\\input' => '/symfony/console/Symfony/Component/Console/Input/Input.php', + 'symfony\\component\\console\\input\\inputargument' => '/symfony/console/Symfony/Component/Console/Input/InputArgument.php', + 'symfony\\component\\console\\input\\inputawareinterface' => '/symfony/console/Symfony/Component/Console/Input/InputAwareInterface.php', + 'symfony\\component\\console\\input\\inputdefinition' => '/symfony/console/Symfony/Component/Console/Input/InputDefinition.php', + 'symfony\\component\\console\\input\\inputinterface' => '/symfony/console/Symfony/Component/Console/Input/InputInterface.php', + 'symfony\\component\\console\\input\\inputoption' => '/symfony/console/Symfony/Component/Console/Input/InputOption.php', + 'symfony\\component\\console\\input\\stringinput' => '/symfony/console/Symfony/Component/Console/Input/StringInput.php', + 'symfony\\component\\console\\output\\bufferedoutput' => '/symfony/console/Symfony/Component/Console/Output/BufferedOutput.php', + 'symfony\\component\\console\\output\\consoleoutput' => '/symfony/console/Symfony/Component/Console/Output/ConsoleOutput.php', + 'symfony\\component\\console\\output\\consoleoutputinterface' => '/symfony/console/Symfony/Component/Console/Output/ConsoleOutputInterface.php', + 'symfony\\component\\console\\output\\nulloutput' => '/symfony/console/Symfony/Component/Console/Output/NullOutput.php', + 'symfony\\component\\console\\output\\output' => '/symfony/console/Symfony/Component/Console/Output/Output.php', + 'symfony\\component\\console\\output\\outputinterface' => '/symfony/console/Symfony/Component/Console/Output/OutputInterface.php', + 'symfony\\component\\console\\output\\streamoutput' => '/symfony/console/Symfony/Component/Console/Output/StreamOutput.php', + 'symfony\\component\\console\\shell' => '/symfony/console/Symfony/Component/Console/Shell.php', + 'symfony\\component\\console\\tester\\applicationtester' => '/symfony/console/Symfony/Component/Console/Tester/ApplicationTester.php', + 'symfony\\component\\console\\tester\\commandtester' => '/symfony/console/Symfony/Component/Console/Tester/CommandTester.php', + 'symfony\\component\\finder\\adapter\\abstractadapter' => '/symfony/finder/Symfony/Component/Finder/Adapter/AbstractAdapter.php', + 'symfony\\component\\finder\\adapter\\abstractfindadapter' => '/symfony/finder/Symfony/Component/Finder/Adapter/AbstractFindAdapter.php', + 'symfony\\component\\finder\\adapter\\adapterinterface' => '/symfony/finder/Symfony/Component/Finder/Adapter/AdapterInterface.php', + 'symfony\\component\\finder\\adapter\\bsdfindadapter' => '/symfony/finder/Symfony/Component/Finder/Adapter/BsdFindAdapter.php', + 'symfony\\component\\finder\\adapter\\gnufindadapter' => '/symfony/finder/Symfony/Component/Finder/Adapter/GnuFindAdapter.php', + 'symfony\\component\\finder\\adapter\\phpadapter' => '/symfony/finder/Symfony/Component/Finder/Adapter/PhpAdapter.php', + 'symfony\\component\\finder\\comparator\\comparator' => '/symfony/finder/Symfony/Component/Finder/Comparator/Comparator.php', + 'symfony\\component\\finder\\comparator\\datecomparator' => '/symfony/finder/Symfony/Component/Finder/Comparator/DateComparator.php', + 'symfony\\component\\finder\\comparator\\numbercomparator' => '/symfony/finder/Symfony/Component/Finder/Comparator/NumberComparator.php', + 'symfony\\component\\finder\\exception\\accessdeniedexception' => '/symfony/finder/Symfony/Component/Finder/Exception/AccessDeniedException.php', + 'symfony\\component\\finder\\exception\\adapterfailureexception' => '/symfony/finder/Symfony/Component/Finder/Exception/AdapterFailureException.php', + 'symfony\\component\\finder\\exception\\exceptioninterface' => '/symfony/finder/Symfony/Component/Finder/Exception/ExceptionInterface.php', + 'symfony\\component\\finder\\exception\\operationnotpermitedexception' => '/symfony/finder/Symfony/Component/Finder/Exception/OperationNotPermitedException.php', + 'symfony\\component\\finder\\exception\\shellcommandfailureexception' => '/symfony/finder/Symfony/Component/Finder/Exception/ShellCommandFailureException.php', + 'symfony\\component\\finder\\expression\\expression' => '/symfony/finder/Symfony/Component/Finder/Expression/Expression.php', + 'symfony\\component\\finder\\expression\\glob' => '/symfony/finder/Symfony/Component/Finder/Expression/Glob.php', + 'symfony\\component\\finder\\expression\\regex' => '/symfony/finder/Symfony/Component/Finder/Expression/Regex.php', + 'symfony\\component\\finder\\expression\\valueinterface' => '/symfony/finder/Symfony/Component/Finder/Expression/ValueInterface.php', + 'symfony\\component\\finder\\finder' => '/symfony/finder/Symfony/Component/Finder/Finder.php', + 'symfony\\component\\finder\\glob' => '/symfony/finder/Symfony/Component/Finder/Glob.php', + 'symfony\\component\\finder\\iterator\\customfilteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/CustomFilterIterator.php', + 'symfony\\component\\finder\\iterator\\daterangefilteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php', + 'symfony\\component\\finder\\iterator\\depthrangefilteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php', + 'symfony\\component\\finder\\iterator\\excludedirectoryfilteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php', + 'symfony\\component\\finder\\iterator\\filecontentfilteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/FilecontentFilterIterator.php', + 'symfony\\component\\finder\\iterator\\filenamefilteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php', + 'symfony\\component\\finder\\iterator\\filepathsiterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/FilePathsIterator.php', + 'symfony\\component\\finder\\iterator\\filetypefilteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/FileTypeFilterIterator.php', + 'symfony\\component\\finder\\iterator\\filteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/FilterIterator.php', + 'symfony\\component\\finder\\iterator\\multiplepcrefilteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php', + 'symfony\\component\\finder\\iterator\\pathfilteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/PathFilterIterator.php', + 'symfony\\component\\finder\\iterator\\recursivedirectoryiterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/RecursiveDirectoryIterator.php', + 'symfony\\component\\finder\\iterator\\sizerangefilteriterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/SizeRangeFilterIterator.php', + 'symfony\\component\\finder\\iterator\\sortableiterator' => '/symfony/finder/Symfony/Component/Finder/Iterator/SortableIterator.php', + 'symfony\\component\\finder\\shell\\command' => '/symfony/finder/Symfony/Component/Finder/Shell/Command.php', + 'symfony\\component\\finder\\shell\\shell' => '/symfony/finder/Symfony/Component/Finder/Shell/Shell.php', + 'symfony\\component\\finder\\splfileinfo' => '/symfony/finder/Symfony/Component/Finder/SplFileInfo.php', + 'symfony\\component\\yaml\\dumper' => '/symfony/yaml/Symfony/Component/Yaml/Dumper.php', + 'symfony\\component\\yaml\\escaper' => '/symfony/yaml/Symfony/Component/Yaml/Escaper.php', + 'symfony\\component\\yaml\\exception\\dumpexception' => '/symfony/yaml/Symfony/Component/Yaml/Exception/DumpException.php', + 'symfony\\component\\yaml\\exception\\exceptioninterface' => '/symfony/yaml/Symfony/Component/Yaml/Exception/ExceptionInterface.php', + 'symfony\\component\\yaml\\exception\\parseexception' => '/symfony/yaml/Symfony/Component/Yaml/Exception/ParseException.php', + 'symfony\\component\\yaml\\exception\\runtimeexception' => '/symfony/yaml/Symfony/Component/Yaml/Exception/RuntimeException.php', + 'symfony\\component\\yaml\\inline' => '/symfony/yaml/Symfony/Component/Yaml/Inline.php', + 'symfony\\component\\yaml\\parser' => '/symfony/yaml/Symfony/Component/Yaml/Parser.php', + 'symfony\\component\\yaml\\unescaper' => '/symfony/yaml/Symfony/Component/Yaml/Unescaper.php', + 'symfony\\component\\yaml\\yaml' => '/symfony/yaml/Symfony/Component/Yaml/Yaml.php', + 'theseer\\fdom\\css\\dollarequalrule' => '/fdomdocument/css/DollarEqualRule.php', + 'theseer\\fdom\\css\\notrule' => '/fdomdocument/css/NotRule.php', + 'theseer\\fdom\\css\\nthchildrule' => '/fdomdocument/css/NthChildRule.php', + 'theseer\\fdom\\css\\regexrule' => '/fdomdocument/css/RegexRule.php', + 'theseer\\fdom\\css\\ruleinterface' => '/fdomdocument/css/RuleInterface.php', + 'theseer\\fdom\\css\\translator' => '/fdomdocument/css/Translator.php', + 'theseer\\fdom\\fdomdocument' => '/fdomdocument/fDOMDocument.php', + 'theseer\\fdom\\fdomdocumentfragment' => '/fdomdocument/fDOMDocumentFragment.php', + 'theseer\\fdom\\fdomelement' => '/fdomdocument/fDOMElement.php', + 'theseer\\fdom\\fdomexception' => '/fdomdocument/fDOMException.php', + 'theseer\\fdom\\fdomnode' => '/fdomdocument/fDOMNode.php', + 'theseer\\fdom\\fdomxpath' => '/fdomdocument/fDOMXPath.php', + 'theseer\\fdom\\xpathquery' => '/fdomdocument/XPathQuery.php', + 'theseer\\fdom\\xpathqueryexception' => '/fdomdocument/XPathQueryException.php' + ); + } + + $class = strtolower($class); + + if (isset($classes[$class])) { + require 'phar://phpdcd-1.0.2.phar' . $classes[$class]; + } + } +); + +Phar::mapPhar('phpdcd-1.0.2.phar'); + +if (isset($_SERVER['argv'][1]) && $_SERVER['argv'][1] == '--manifest') { + print file_get_contents(__PHPDCD_PHAR_ROOT__ . '/manifest.txt'); + exit; +} + +$application = new SebastianBergmann\PHPDCD\CLI\Application; +$application->run(); + +__HALT_COMPILER(); ?> +)|phpdcd-1.0.2.pharCsymfony/finder/Symfony/Component/Finder/Adapter/AbstractAdapter.php\S Bsymfony/finder/Symfony/Component/Finder/Adapter/GnuFindAdapter.php +\S +}]Bsymfony/finder/Symfony/Component/Finder/Adapter/BsdFindAdapter.php +\S +kAGsymfony/finder/Symfony/Component/Finder/Adapter/AbstractFindAdapter.php)\S)QJ>symfony/finder/Symfony/Component/Finder/Adapter/PhpAdapter.php{ +\S{ +9!2Dsymfony/finder/Symfony/Component/Finder/Adapter/AdapterInterface.php \S 0symfony/finder/Symfony/Component/Finder/Glob.phpb \Sb g@MFsymfony/finder/Symfony/Component/Finder/Iterator/FilePathsIterator.php +\S +ɔKsymfony/finder/Symfony/Component/Finder/Iterator/FileTypeFilterIterator.phpb\Sbn Esymfony/finder/Symfony/Component/Finder/Iterator/SortableIterator.php \S jCsymfony/finder/Symfony/Component/Finder/Iterator/FilterIterator.php\S,GNsymfony/finder/Symfony/Component/Finder/Iterator/FilecontentFilterIterator.php\SʽLsymfony/finder/Symfony/Component/Finder/Iterator/SizeRangeFilterIterator.php\SxPSsymfony/finder/Symfony/Component/Finder/Iterator/ExcludeDirectoryFilterIterator.php\SdMsymfony/finder/Symfony/Component/Finder/Iterator/DepthRangeFilterIterator.php\Sils_Osymfony/finder/Symfony/Component/Finder/Iterator/MultiplePcreFilterIterator.php\S + +Osymfony/finder/Symfony/Component/Finder/Iterator/RecursiveDirectoryIterator.php \S 6sKsymfony/finder/Symfony/Component/Finder/Iterator/FilenameFilterIterator.php\S{^ǶIsymfony/finder/Symfony/Component/Finder/Iterator/CustomFilterIterator.php\Sz"Gsymfony/finder/Symfony/Component/Finder/Iterator/PathFilterIterator.php\S)Lsymfony/finder/Symfony/Component/Finder/Iterator/DateRangeFilterIterator.php\S UGsymfony/finder/Symfony/Component/Finder/Comparator/NumberComparator.php +\S +PAsymfony/finder/Symfony/Component/Finder/Comparator/Comparator.php +\S +|FzEsymfony/finder/Symfony/Component/Finder/Comparator/DateComparator.php\S 9symfony/finder/Symfony/Component/Finder/Shell/Command.php\S5K7symfony/finder/Symfony/Component/Finder/Shell/Shell.php\S4˵Ssymfony/finder/Symfony/Component/Finder/Exception/OperationNotPermitedException.php\SPMsymfony/finder/Symfony/Component/Finder/Exception/AdapterFailureException.php\S>@Ksymfony/finder/Symfony/Component/Finder/Exception/AccessDeniedException.php\ScW޶Hsymfony/finder/Symfony/Component/Finder/Exception/ExceptionInterface.php\S7Rsymfony/finder/Symfony/Component/Finder/Exception/ShellCommandFailureException.php9\S92symfony/finder/Symfony/Component/Finder/Finder.phpV\SVd7symfony/finder/Symfony/Component/Finder/SplFileInfo.php\S59Esymfony/finder/Symfony/Component/Finder/Expression/ValueInterface.phpH\SH";symfony/finder/Symfony/Component/Finder/Expression/Glob.php\SEAsymfony/finder/Symfony/Component/Finder/Expression/Expression.php& +\S& + 뇶<symfony/finder/Symfony/Component/Finder/Expression/Regex.php\S(ڶ,symfony/yaml/Symfony/Component/Yaml/Yaml.php \S 7MӺ/symfony/yaml/Symfony/Component/Yaml/Escaper.phpm \Sm )%,.symfony/yaml/Symfony/Component/Yaml/Parser.php[\S[X.symfony/yaml/Symfony/Component/Yaml/Dumper.php \S 8n1symfony/yaml/Symfony/Component/Yaml/Unescaper.php\S6H ?symfony/yaml/Symfony/Component/Yaml/Exception/DumpException.php\Sؙ՚Dsymfony/yaml/Symfony/Component/Yaml/Exception/ExceptionInterface.php\S+l@symfony/yaml/Symfony/Component/Yaml/Exception/ParseException.php \S Bsymfony/yaml/Symfony/Component/Yaml/Exception/RuntimeException.php\S|-.symfony/yaml/Symfony/Component/Yaml/Inline.php?\S?q+3;symfony/console/Symfony/Component/Console/ConsoleEvents.php\S\0EGsymfony/console/Symfony/Component/Console/Formatter/OutputFormatter.php\SlPsymfony/console/Symfony/Component/Console/Formatter/OutputFormatterInterface.php\SJp˶Lsymfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyle.php&\S&onjUsymfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleInterface.php\S5Qsymfony/console/Symfony/Component/Console/Formatter/OutputFormatterStyleStack.php +\S +v Dsymfony/console/Symfony/Component/Console/Helper/HelperInterface.php\Sz׶Dsymfony/console/Symfony/Component/Console/Helper/FormatterHelper.php\Sgse@symfony/console/Symfony/Component/Console/Helper/TableHelper.php2\S2Esymfony/console/Symfony/Component/Console/Helper/DescriptorHelper.php +\S +"U1Esymfony/console/Symfony/Component/Console/Helper/InputAwareHelper.php\Ss HCsymfony/console/Symfony/Component/Console/Helper/ProgressHelper.phpo.\So.|pAsymfony/console/Symfony/Component/Console/Helper/DialogHelper.php>@\S>@&;symfony/console/Symfony/Component/Console/Helper/Helper.php\SZ\>symfony/console/Symfony/Component/Console/Helper/HelperSet.php \S Bsymfony/console/Symfony/Component/Console/Tester/CommandTester.php\S~H(RFsymfony/console/Symfony/Component/Console/Tester/ApplicationTester.phpf \Sf hrKsymfony/console/Symfony/Component/Console/Output/ConsoleOutputInterface.phpK\SK0;symfony/console/Symfony/Component/Console/Output/Output.php;\S;sQAsymfony/console/Symfony/Component/Console/Output/StreamOutput.phpM \SM ,QBsymfony/console/Symfony/Component/Console/Output/ConsoleOutput.php} \S} ϱ?symfony/console/Symfony/Component/Console/Output/NullOutput.php\SF$_Csymfony/console/Symfony/Component/Console/Output/BufferedOutput.phph\Sht|X4Dsymfony/console/Symfony/Component/Console/Output/OutputInterface.phpM \SM zCsymfony/console/Symfony/Component/Console/Descriptor/Descriptor.php \S mFsymfony/console/Symfony/Component/Console/Descriptor/XmlDescriptor.php%\S%ﶲͶGsymfony/console/Symfony/Component/Console/Descriptor/TextDescriptor.php\SKsymfony/console/Symfony/Component/Console/Descriptor/MarkdownDescriptor.php.\S.lPOsymfony/console/Symfony/Component/Console/Descriptor/ApplicationDescription.php \S sXGsymfony/console/Symfony/Component/Console/Descriptor/JsonDescriptor.phpu\SuDLsymfony/console/Symfony/Component/Console/Descriptor/DescriptorInterface.php\SJZ0<Isymfony/console/Symfony/Component/Console/Event/ConsoleTerminateEvent.php&\S&Z@symfony/console/Symfony/Component/Console/Event/ConsoleEvent.php\S +Gsymfony/console/Symfony/Component/Console/Event/ConsoleCommandEvent.php\S||-&Isymfony/console/Symfony/Component/Console/Event/ConsoleExceptionEvent.phpA\SAsK>symfony/console/Symfony/Component/Console/Input/ArrayInput.php%\S%p?symfony/console/Symfony/Component/Console/Input/StringInput.php +\S +΂=Csymfony/console/Symfony/Component/Console/Input/InputDefinition.php(0\S(01EBsymfony/console/Symfony/Component/Console/Input/InputInterface.php6\S6AMAsymfony/console/Symfony/Component/Console/Input/InputArgument.php \S 'Gsymfony/console/Symfony/Component/Console/Input/InputAwareInterface.php^\S^9Kh?symfony/console/Symfony/Component/Console/Input/InputOption.phpV\SVOö9symfony/console/Symfony/Component/Console/Input/Input.php\Sgm(=symfony/console/Symfony/Component/Console/Input/ArgvInput.php)\S)QAsymfony/console/Symfony/Component/Console/Command/HelpCommand.php_ +\S_ +m=symfony/console/Symfony/Component/Console/Command/Command.php7@\S7@ƘAsymfony/console/Symfony/Component/Console/Command/ListCommand.php +\S + x9symfony/console/Symfony/Component/Console/Application.phpҊ\SҊLU3symfony/console/Symfony/Component/Console/Shell.php\S2Vsrc/Log/Text.php \S nٶڶsrc/Analyser.phpE7\SE70Qsrc/Detector.php\SwHx?src/CLI/Command.php&\S&UIsrc/CLI/Application.php,\S,<U manifest.txt\SʵͶversion/Version.php \S Y finder-facade/FinderFacade.php\SՍfinder-facade/Configuration.php{\S{rfdomdocument/fDOMElement.php@0\S@0,Gfdomdocument/fDOMNode.php\Sוfdomdocument/XPathQuery.phpj\Sjfdomdocument/css/NotRule.php\S!8_fdomdocument/css/Translator.php\S0L3!fdomdocument/css/NthChildRule.phpk\Skf]fdomdocument/css/RegexRule.php\Ss$fdomdocument/css/DollarEqualRule.phpw\Sw} ض"fdomdocument/css/RuleInterface.php\S,fdomdocument/fDOMXPath.php\S<[$fdomdocument/XPathQueryException.php\Sֶfdomdocument/fDOMException.php\SB%fdomdocument/autoload.php\S!Hfdomdocument/fDOMDocument.phpR\SRe?%fdomdocument/fDOMDocumentFragment.php: \S: + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Finder\Adapter; + +/** + * Interface for finder engine implementations. + * + * @author Jean-François Simon + */ +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 = (Boolean) $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 Boolean 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; + +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 + */ +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; + +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 + */ +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; + +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\Iterator\SortableIterator; +use Symfony\Component\Finder\Comparator\NumberComparator; +use Symfony\Component\Finder\Comparator\DateComparator; + +/** + * Shell engine implementation using GNU find command. + * + * @author Jean-François Simon + */ +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) { return; } + : 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 Boolean $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 Boolean $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() ? $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'); + 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 Boolean $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; + +use Symfony\Component\Finder\Iterator; + +/** + * PHP finder engine implementation. + * + * @author Jean-François Simon + */ +class PhpAdapter extends AbstractAdapter +{ + /** + * {@inheritdoc} + */ + public function searchInDirectory($dir) + { + $flags = \RecursiveDirectoryIterator::SKIP_DOTS; + + if ($this->followLinks) { + $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; + } + + $iterator = new \RecursiveIteratorIterator( + new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs), + \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->exclude) { + $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude); + } + + 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->sort) { + $iteratorAggregate = new Iterator\SortableIterator($iterator, $this->sort); + $iterator = $iteratorAggregate->getIterator(); + } + + if ($this->paths || $this->notPaths) { + $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $this->notPaths); + } + + 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\Adapter; + +/** + * @author Jean-François Simon + */ +interface AdapterInterface +{ + /** + * @param Boolean $followLinks + * + * @return AdapterInterface Current instance + */ + public function setFollowLinks($followLinks); + + /** + * @param integer $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|integer $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 boolean $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 Boolean + */ + 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; + +/** + * 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 Boolean $strictLeadingDot + * @param Boolean $strictWildcardSlash + * + * @return string regex The regexp + */ + public static function toRegex($glob, $strictLeadingDot = true, $strictWildcardSlash = true) + { + $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 ('.' === $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 '#^'.$regex.'$#'; + } +} + + * + * 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\SplFileInfo; + +/** + * Iterate over shell command result. + * + * @author Jean-François Simon + */ +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 integer $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 Boolean 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; + +/** + * 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 integer|callback $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(), $b->getRealpath()); + }; + } 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(), $b->getRealpath()); + }; + } 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 callback 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\Iterator; + +/** + * This iterator just overrides the rewind method in order to correct a PHP bug. + * + * @see https://bugs.php.net/bug.php?id=49104 + * + * @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() + { + $iterator = $this; + while ($iterator instanceof \OuterIterator) { + $innerIterator = $iterator->getInnerIterator(); + + if ($innerIterator instanceof RecursiveDirectoryIterator) { + if ($innerIterator->isRewindable()) { + $innerIterator->next(); + $innerIterator->rewind(); + } + } elseif ($iterator->getInnerIterator() instanceof \FilesystemIterator) { + $iterator->getInnerIterator()->next(); + $iterator->getInnerIterator()->rewind(); + } + $iterator = $iterator->getInnerIterator(); + } + + 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; + +/** + * 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 Boolean 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; + } + + // should at least not match one rule to exclude + foreach ($this->noMatchRegexps as $regex) { + if (preg_match($regex, $content)) { + return false; + } + } + + // should at least match one rule + $match = true; + if ($this->matchRegexps) { + $match = false; + foreach ($this->matchRegexps as $regex) { + if (preg_match($regex, $content)) { + return true; + } + } + } + + return $match; + } + + /** + * 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\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 Boolean 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; + +/** + * ExcludeDirectoryFilterIterator filters out directories. + * + * @author Fabien Potencier + */ +class ExcludeDirectoryFilterIterator extends FilterIterator +{ + private $patterns = array(); + + /** + * Constructor. + * + * @param \Iterator $iterator The Iterator to filter + * @param array $directories An array of directories to exclude + */ + public function __construct(\Iterator $iterator, array $directories) + { + foreach ($directories as $directory) { + $this->patterns[] = '#(^|/)'.preg_quote($directory, '#').'(/|$)#'; + } + + parent::__construct($iterator); + } + + /** + * Filters the iterator values. + * + * @return Boolean true if the value should be kept, false otherwise + */ + public function accept() + { + $path = $this->isDir() ? $this->current()->getRelativePathname() : $this->current()->getRelativePath(); + $path = strtr($path, '\\', '/'); + foreach ($this->patterns as $pattern) { + if (preg_match($pattern, $path)) { + 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 Boolean 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; + +use Symfony\Component\Finder\Expression\Expression; + +/** + * 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 a regex. + * + * @param string $str + * + * @return Boolean Whether the given string is a regex + */ + protected function isRegex($str) + { + return Expression::create($str)->isRegex(); + } + + /** + * 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; + +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 boolean + */ + private $ignoreUnreadableDirs; + + /** + * @var Boolean + */ + private $rewindable; + + /** + * Constructor. + * + * @param string $path + * @param int $flags + * @param boolean $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; + } + + /** + * Return an instance of SplFileInfo with support for relative paths + * + * @return SplFileInfo File information + */ + public function current() + { + return new SplFileInfo(parent::current()->getPathname(), $this->getSubPath(), $this->getSubPathname()); + } + + /** + * @return \RecursiveIterator + * + * @throws AccessDeniedException + */ + public function getChildren() + { + try { + return parent::getChildren(); + } 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/bug.php?id=49104 + parent::next(); + + parent::rewind(); + } + + /** + * Checks if the stream is rewindable. + * + * @return Boolean true when the stream is rewindable, false otherwise + */ + public function isRewindable() + { + if (null !== $this->rewindable) { + return $this->rewindable; + } + + 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\Expression\Expression; + +/** + * FilenameFilterIterator filters files by patterns (a regexp, a glob, or a string). + * + * @author Fabien Potencier + */ +class FilenameFilterIterator extends MultiplePcreFilterIterator +{ + + /** + * Filters the iterator values. + * + * @return Boolean true if the value should be kept, false otherwise + */ + public function accept() + { + $filename = $this->current()->getFilename(); + + // should at least not match one rule to exclude + foreach ($this->noMatchRegexps as $regex) { + if (preg_match($regex, $filename)) { + return false; + } + } + + // should at least match one rule + $match = true; + if ($this->matchRegexps) { + $match = false; + foreach ($this->matchRegexps as $regex) { + if (preg_match($regex, $filename)) { + return true; + } + } + } + + return $match; + } + + /** + * 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 Expression::create($str)->getRegex()->render(); + } +} + + * + * 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 array $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 Boolean 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; + +/** + * 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 Boolean true if the value should be kept, false otherwise + */ + public function accept() + { + $filename = $this->current()->getRelativePathname(); + + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + $filename = strtr($filename, '\\', '/'); + } + + // should at least not match one rule to exclude + foreach ($this->noMatchRegexps as $regex) { + if (preg_match($regex, $filename)) { + return false; + } + } + + // should at least match one rule + $match = true; + if ($this->matchRegexps) { + $match = false; + foreach ($this->matchRegexps as $regex) { + if (preg_match($regex, $filename)) { + return true; + } + } + } + + return $match; + } + + /** + * 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\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 Boolean true if the value should be kept, false otherwise + */ + public function accept() + { + $fileinfo = $this->current(); + + if (!$fileinfo->isFile()) { + return true; + } + + $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\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 $test A comparison string + * + * @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\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 Boolean + */ + 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\Shell; + +/** + * @author Jean-François Simon + */ +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 === $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])) { + call_user_func($this->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 integer $index + * + * @return Command The current Command instance + */ + public function addAtIndex($bit, $index) + { + array_splice($this->bits, $index, 0, $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; + +/** + * @author Jean-François Simon + */ +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 (self::TYPE_WINDOWS === $this->type) { + // todo: find a way to test if Windows command exists + return false; + } + + if (!function_exists('exec')) { + return false; + } + + // todo: find a better way (command could not be available) + exec('command -v '.$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\Exception; + +/** + * @author Jean-François Simon + */ +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; + +use Symfony\Component\Finder\Adapter\AdapterInterface; + +/** + * Base exception for all adapter failures. + * + * @author Jean-François Simon + */ +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 + */ +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; + +/** + * @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; + +use Symfony\Component\Finder\Adapter\AdapterInterface; +use Symfony\Component\Finder\Shell\Command; + +/** + * @author Jean-François Simon + */ +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; + +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\Exception\ExceptionInterface; + +/** + * 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 + * + * @api + */ +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 = array(); + 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; + + $this + ->addAdapter(new GnuFindAdapter()) + ->addAdapter(new BsdFindAdapter()) + ->addAdapter(new PhpAdapter(), -50) + ->setAdapter('php') + ; + } + + /** + * Creates a new Finder. + * + * @return Finder A new Finder instance + * + * @api + */ + public static function create() + { + return new static(); + } + + /** + * Registers a finder engine implementation. + * + * @param AdapterInterface $adapter An adapter instance + * @param integer $priority Highest is selected first + * + * @return Finder The current Finder instance + */ + public function addAdapter(Adapter\AdapterInterface $adapter, $priority = 0) + { + $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 + */ + public function useBestAdapter() + { + $this->resetAdapterSelection(); + + return $this->sortAdapters(); + } + + /** + * Selects the adapter to use. + * + * @param string $name + * + * @throws \InvalidArgumentException + * + * @return Finder The current Finder instance + */ + public function setAdapter($name) + { + 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 + */ + public function removeAdapters() + { + $this->adapters = array(); + + return $this; + } + + /** + * Returns registered adapters ordered by priority without extra information. + * + * @return AdapterInterface[] + */ + public function getAdapters() + { + return array_values(array_map(function (array $adapter) { + return $adapter['adapter']; + }, $this->adapters)); + } + + /** + * Restricts the matching to directories only. + * + * @return Finder The current Finder instance + * + * @api + */ + public function directories() + { + $this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES; + + return $this; + } + + /** + * Restricts the matching to files only. + * + * @return Finder The current Finder instance + * + * @api + */ + 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 int $level The depth level expression + * + * @return Finder The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\DepthRangeFilterIterator + * @see Symfony\Component\Finder\Comparator\NumberComparator + * + * @api + */ + 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 rage string + * + * @return Finder The current Finder instance + * + * @see strtotime + * @see Symfony\Component\Finder\Iterator\DateRangeFilterIterator + * @see Symfony\Component\Finder\Comparator\DateComparator + * + * @api + */ + 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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\FilenameFilterIterator + * + * @api + */ + 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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\FilenameFilterIterator + * + * @api + */ + 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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\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 $size A size range string + * + * @return Finder The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\SizeRangeFilterIterator + * @see Symfony\Component\Finder\Comparator\NumberComparator + * + * @api + */ + 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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator + * + * @api + */ + public function exclude($dirs) + { + $this->exclude = array_merge($this->exclude, (array) $dirs); + + return $this; + } + + /** + * Excludes "hidden" directories and files (starting with a dot). + * + * @param Boolean $ignoreDotFiles Whether to exclude "hidden" files or not + * + * @return Finder The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator + * + * @api + */ + public function ignoreDotFiles($ignoreDotFiles) + { + if ($ignoreDotFiles) { + $this->ignore = $this->ignore | static::IGNORE_DOT_FILES; + } else { + $this->ignore = $this->ignore & ~static::IGNORE_DOT_FILES; + } + + return $this; + } + + /** + * Forces the finder to ignore version control directories. + * + * @param Boolean $ignoreVCS Whether to exclude VCS files or not + * + * @return Finder The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator + * + * @api + */ + public function ignoreVCS($ignoreVCS) + { + if ($ignoreVCS) { + $this->ignore = $this->ignore | static::IGNORE_VCS_FILES; + } else { + $this->ignore = $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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\SortableIterator + * + * @api + */ + 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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\SortableIterator + * + * @api + */ + 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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\SortableIterator + * + * @api + */ + 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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\SortableIterator + * + * @api + */ + 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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\SortableIterator + * + * @api + */ + 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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\SortableIterator + * + * @api + */ + 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 The current Finder instance + * + * @see Symfony\Component\Finder\Iterator\CustomFilterIterator + * + * @api + */ + public function filter(\Closure $closure) + { + $this->filters[] = $closure; + + return $this; + } + + /** + * Forces the following of symlinks. + * + * @return Finder The current Finder instance + * + * @api + */ + public function followLinks() + { + $this->followLinks = true; + + return $this; + } + + /** + * Tells finder to ignore unreadable directories. + * + * By default, scanning unreadable directories content throws an AccessDeniedException. + * + * @param boolean $ignore + * + * @return Finder The current Finder instance + */ + public function ignoreUnreadableDirs($ignore = true) + { + $this->ignoreUnreadableDirs = (Boolean) $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 The current Finder instance + * + * @throws \InvalidArgumentException if one of the directories does not exist + * + * @api + */ + public function in($dirs) + { + $resolvedDirs = array(); + + foreach ((array) $dirs as $dir) { + if (is_dir($dir)) { + $resolvedDirs[] = $dir; + } elseif ($glob = glob($dir, 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 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 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 + * + * @throws \RuntimeException When none of the adapters are supported + */ + 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[] = '#(^|/)\..+(/|$)#'; + } + + foreach ($this->adapters as $adapter) { + if ($adapter['adapter']->isSupported()) { + try { + return $this + ->buildAdapter($adapter['adapter']) + ->searchInDirectory($dir); + } catch (ExceptionInterface $e) {} + } + } + + throw new \RuntimeException('No supported adapter found.'); + } + + /** + * @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); + } +} + + * + * 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 + * + * @return string the relative path + */ + public function getRelativePath() + { + return $this->relativePath; + } + + /** + * Returns the relative path 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. + */ + +namespace Symfony\Component\Finder\Expression; + +/** + * @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\Expression; + +/** + * @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) + { + $firstByte = true; + $escaping = false; + $inCurlies = 0; + $regex = ''; + $sizeGlob = strlen($this->pattern); + for ($i = 0; $i < $sizeGlob; $i++) { + $car = $this->pattern[$i]; + if ($firstByte) { + if ($strictLeadingDot && '.' !== $car) { + $regex .= '(?=[^\.])'; + } + + $firstByte = false; + } + + if ('/' === $car) { + $firstByte = true; + } + + if ('.' === $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 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; + +/** + * @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(); + } + + /** + * @throws \LogicException + * + * @return Glob + */ + 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; + +/** + * @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\Yaml; + +use Symfony\Component\Yaml\Exception\ParseException; + +/** + * Yaml offers convenience methods to load and dump YAML. + * + * @author Fabien Potencier + * + * @api + */ +class Yaml +{ + /** + * Parses YAML into a PHP array. + * + * The parse method, when supplied with a YAML stream (string or file), + * will do its best to convert YAML in a file into a PHP array. + * + * Usage: + * + * $array = Yaml::parse('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. + * + * @param string $input Path to a YAML file or a string containing YAML + * @param Boolean $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise + * @param Boolean $objectSupport True if object support is enabled, false otherwise + * + * @return array The YAML converted to a PHP array + * + * @throws ParseException If the YAML is not valid + * + * @api + */ + public static function parse($input, $exceptionOnInvalidType = false, $objectSupport = false) + { + // if input is a file, process it + $file = ''; + if (strpos($input, "\n") === false && is_file($input)) { + 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); + } catch (ParseException $e) { + if ($file) { + $e->setParsedFile($file); + } + + throw $e; + } + } + + /** + * Dumps a PHP array to a YAML string. + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. + * + * @param array $array PHP array + * @param integer $inline The level where you switch to inline YAML + * @param integer $indent The amount of spaces to use for indentation of nested nodes. + * @param Boolean $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param Boolean $objectSupport true if object support is enabled, false otherwise + * + * @return string A YAML string representing the original PHP array + * + * @api + */ + public static function dump($array, $inline = 2, $indent = 4, $exceptionOnInvalidType = false, $objectSupport = false) + { + $yaml = new Dumper(); + $yaml->setIndentation($indent); + + return $yaml->dump($array, $inline, 0, $exceptionOnInvalidType, $objectSupport); + } +} + + * + * 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 + */ +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 Boolean 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 Boolean True if the value would require single quotes. + */ + public static function requiresSingleQuoting($value) + { + 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; + +use Symfony\Component\Yaml\Exception\ParseException; + +/** + * Parser parses YAML strings to convert them to PHP arrays. + * + * @author Fabien Potencier + */ +class Parser +{ + const FOLDED_SCALAR_PATTERN = '(?P\||>)(?P\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P +#.*)?'; + + private $offset = 0; + private $lines = array(); + private $currentLineNb = -1; + private $currentLine = ''; + private $refs = array(); + + /** + * Constructor + * + * @param integer $offset The offset of YAML document (used for line numbers in error messages) + */ + public function __construct($offset = 0) + { + $this->offset = $offset; + } + + /** + * Parses a YAML string to a PHP value. + * + * @param string $value A YAML string + * @param Boolean $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param Boolean $objectSupport true if object support is enabled, false otherwise + * + * @return mixed A PHP value + * + * @throws ParseException If the YAML is not valid + */ + public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false) + { + $this->currentLineNb = -1; + $this->currentLine = ''; + $this->lines = explode("\n", $this->cleanup($value)); + + if (function_exists('mb_detect_encoding') && false === mb_detect_encoding($value, 'UTF-8', true)) { + throw new ParseException('The YAML value does not appear to be valid UTF-8.'); + } + + if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) { + $mbEncoding = mb_internal_encoding(); + mb_internal_encoding('UTF-8'); + } + + $data = array(); + $context = null; + 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 = $isInPlace = $isProcessed = 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'); + } + $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'], ' '), '#')) { + $c = $this->getRealCurrentLineNb() + 1; + $parser = new Parser($c); + $parser->refs =& $this->refs; + $data[] = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport); + } else { + if (isset($values['leadspaces']) + && ' ' == $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 + $c = $this->getRealCurrentLineNb(); + $parser = new Parser($c); + $parser->refs =& $this->refs; + + $block = $values['value']; + if ($this->isNextLineIndented()) { + $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + 2); + } + + $data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport); + } else { + $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport); + } + } + } elseif (preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P.+?))?\s*$#u', $this->currentLine, $values) && false === strpos($values['key'],' #')) { + if ($context && 'sequence' == $context) { + throw new ParseException('You cannot define a mapping item when in a sequence'); + } + $context = 'mapping'; + + // force correct settings + Inline::parse(null, $exceptionOnInvalidType, $objectSupport); + try { + $key = Inline::parseScalar($values['key']); + } catch (ParseException $e) { + $e->setParsedLine($this->getRealCurrentLineNb() + 1); + $e->setSnippet($this->currentLine); + + throw $e; + } + + if ('<<' === $key) { + if (isset($values['value']) && 0 === strpos($values['value'], '*')) { + $isInPlace = substr($values['value'], 1); + if (!array_key_exists($isInPlace, $this->refs)) { + throw new ParseException(sprintf('Reference "%s" does not exist.', $isInPlace), $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + } else { + if (isset($values['value']) && $values['value'] !== '') { + $value = $values['value']; + } else { + $value = $this->getNextEmbedBlock(); + } + $c = $this->getRealCurrentLineNb() + 1; + $parser = new Parser($c); + $parser->refs =& $this->refs; + $parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport); + + $merged = array(); + if (!is_array($parsed)) { + throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + } elseif (isset($parsed[0])) { + // Numeric array, merge individual elements + foreach (array_reverse($parsed) as $parsedItem) { + if (!is_array($parsedItem)) { + throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem); + } + $merged = array_merge($parsedItem, $merged); + } + } else { + // Associative array, merge + $merged = array_merge($merged, $parsed); + } + + $isProcessed = $merged; + } + } elseif (isset($values['value']) && preg_match('#^&(?P[^ ]+) *(?P.*)#u', $values['value'], $matches)) { + $isRef = $matches['ref']; + $values['value'] = $matches['value']; + } + + if ($isProcessed) { + // Merge keys + $data = $isProcessed; + // hash + } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) { + // if next line is less indented or equal, then it means that the current value is null + if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) { + $data[$key] = null; + } else { + $c = $this->getRealCurrentLineNb() + 1; + $parser = new Parser($c); + $parser->refs =& $this->refs; + $data[$key] = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport); + } + } else { + if ($isInPlace) { + $data = $this->refs[$isInPlace]; + } else { + $data[$key] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport); + } + } + } else { + // 1-liner optionally followed by newline + $lineCount = count($this->lines); + if (1 === $lineCount || (2 === $lineCount && empty($this->lines[1]))) { + try { + $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport); + } catch (ParseException $e) { + $e->setParsedLine($this->getRealCurrentLineNb() + 1); + $e->setSnippet($this->currentLine); + + throw $e; + } + + if (is_array($value)) { + $first = reset($value); + if (is_string($first) && 0 === strpos($first, '*')) { + $data = array(); + foreach ($value as $alias) { + $data[] = $this->refs[substr($alias, 1)]; + } + $value = $data; + } + } + + 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 ($isRef) { + $this->refs[$isRef] = end($data); + } + } + + if (isset($mbEncoding)) { + mb_internal_encoding($mbEncoding); + } + + return empty($data) ? null : $data; + } + + /** + * Returns the current line number (takes the offset into account). + * + * @return integer The current line number + */ + private function getRealCurrentLineNb() + { + return $this->currentLineNb + $this->offset; + } + + /** + * Returns the current line indentation. + * + * @return integer The current line indentation + */ + private function getCurrentLineIndentation() + { + return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' ')); + } + + /** + * Returns the next embed block of YAML. + * + * @param integer $indentation The indent level at which the block is to be read, or null for default + * + * @return string A YAML string + * + * @throws ParseException When indentation problem are detected + */ + private function getNextEmbedBlock($indentation = null) + { + $this->moveToNextLine(); + + if (null === $indentation) { + $newIndent = $this->getCurrentLineIndentation(); + + $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem($this->currentLine); + + if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) { + throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); + } + } else { + $newIndent = $indentation; + } + + $data = array(substr($this->currentLine, $newIndent)); + + $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem($this->currentLine); + + // Comments must not be removed inside a string block (ie. after a line ending with "|") + $removeCommentsPattern = '~'.self::FOLDED_SCALAR_PATTERN.'$~'; + $removeComments = !preg_match($removeCommentsPattern, $this->currentLine); + + while ($this->moveToNextLine()) { + $indent = $this->getCurrentLineIndentation(); + + if ($indent === $newIndent) { + $removeComments = !preg_match($removeCommentsPattern, $this->currentLine); + } + + if ($isItUnindentedCollection && !$this->isStringUnIndentedCollectionItem($this->currentLine)) { + $this->moveToPreviousLine(); + break; + } + + if ($this->isCurrentLineBlank()) { + $data[] = substr($this->currentLine, $newIndent); + continue; + } + + if ($removeComments && $this->isCurrentLineComment()) { + 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 Boolean + */ + 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. + */ + private function moveToPreviousLine() + { + $this->currentLine = $this->lines[--$this->currentLineNb]; + } + + /** + * Parses a YAML value. + * + * @param string $value A YAML value + * @param Boolean $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise + * @param Boolean $objectSupport True if object support is enabled, false otherwise + * + * @return mixed A PHP value + * + * @throws ParseException When reference does not exist + */ + private function parseValue($value, $exceptionOnInvalidType, $objectSupport) + { + 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->currentLine); + } + + return $this->refs[$value]; + } + + if (preg_match('/^'.self::FOLDED_SCALAR_PATTERN.'$/', $value, $matches)) { + $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : ''; + + return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), intval(abs($modifiers))); + } + + try { + return Inline::parse($value, $exceptionOnInvalidType, $objectSupport); + } catch (ParseException $e) { + $e->setParsedLine($this->getRealCurrentLineNb() + 1); + $e->setSnippet($this->currentLine); + + throw $e; + } + } + + /** + * Parses a folded scalar. + * + * @param string $separator The separator that was used to begin this folded scalar (| or >) + * @param string $indicator The indicator that was used to begin this folded scalar (+ or -) + * @param integer $indentation The indentation that was used to begin this folded scalar + * + * @return string The text value + */ + private function parseFoldedScalar($separator, $indicator = '', $indentation = 0) + { + $notEOF = $this->moveToNextLine(); + if (!$notEOF) { + return ''; + } + + $isCurrentLineBlank = $this->isCurrentLineBlank(); + $text = ''; + + // leading blank lines are consumed before determining indentation + while ($notEOF && $isCurrentLineBlank) { + // newline only if not EOF + if ($notEOF = $this->moveToNextLine()) { + $text .= "\n"; + $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) { + $text .= substr($this->currentLine, $indentation); + } else { + $text .= $matches[1]; + } + + // newline only if not EOF + if ($notEOF = $this->moveToNextLine()) { + $text .= "\n"; + $isCurrentLineBlank = $this->isCurrentLineBlank(); + } + } + } elseif ($notEOF) { + $text .= "\n"; + } + + if ($notEOF) { + $this->moveToPreviousLine(); + } + + // replace all non-trailing single newlines with spaces in folded blocks + if ('>' === $separator) { + preg_match('/(\n*)$/', $text, $matches); + $text = preg_replace('/(?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 Boolean 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 Boolean 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 Boolean 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[0] === '#'; + } + + /** + * 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#su', '', $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*$#s', '', $value); + } + + return $value; + } + + /** + * Returns true if the next line starts unindented collection + * + * @return Boolean 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($this->currentLine) + ) { + $ret = true; + } + + $this->moveToPreviousLine(); + + return $ret; + } + + /** + * Returns true if the string is un-indented collection item + * + * @return Boolean Returns true if the string is un-indented collection item, false otherwise + */ + private function isStringUnIndentedCollectionItem() + { + return (0 === strpos($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; + +/** + * Dumper dumps PHP variables to YAML strings. + * + * @author Fabien Potencier + */ +class Dumper +{ + /** + * The amount of spaces to use for indentation of nested nodes. + * + * @var integer + */ + protected $indentation = 4; + + /** + * Sets the indentation. + * + * @param integer $num The amount of spaces to use for indentation of nested nodes. + */ + public function setIndentation($num) + { + $this->indentation = (int) $num; + } + + /** + * Dumps a PHP value to YAML. + * + * @param mixed $input The PHP value + * @param integer $inline The level where you switch to inline YAML + * @param integer $indent The level of indentation (used internally) + * @param Boolean $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param Boolean $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 = array_keys($input) !== range(0, count($input) - 1); + + 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; + +/** + * Unescaper encapsulates unescaping rules for single and double-quoted + * YAML strings. + * + * @author Matthew Lewinski + */ +class Unescaper +{ + // Parser and Inline assume UTF-8 encoding, so escaped Unicode characters + // must be converted to that encoding. + const ENCODING = 'UTF-8'; + + // Regex fragment that matches an escaped character in a double quoted + // string. + const REGEX_ESCAPED_CHARACTER = "\\\\([0abt\tnvfre \\\"\\/\\\\N_LP]|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 + */ + 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 "\xd"; + case 'e': + return "\x1b"; + case ' ': + return ' '; + case '"': + return '"'; + case '/': + return '/'; + case '\\': + return '\\'; + case 'N': + // U+0085 NEXT LINE + return $this->convertEncoding("\x00\x85", self::ENCODING, 'UCS-2BE'); + case '_': + // U+00A0 NO-BREAK SPACE + return $this->convertEncoding("\x00\xA0", self::ENCODING, 'UCS-2BE'); + case 'L': + // U+2028 LINE SEPARATOR + return $this->convertEncoding("\x20\x28", self::ENCODING, 'UCS-2BE'); + case 'P': + // U+2029 PARAGRAPH SEPARATOR + return $this->convertEncoding("\x20\x29", self::ENCODING, 'UCS-2BE'); + case 'x': + $char = pack('n', hexdec(substr($value, 2, 2))); + + return $this->convertEncoding($char, self::ENCODING, 'UCS-2BE'); + case 'u': + $char = pack('n', hexdec(substr($value, 2, 4))); + + return $this->convertEncoding($char, self::ENCODING, 'UCS-2BE'); + case 'U': + $char = pack('N', hexdec(substr($value, 2, 8))); + + return $this->convertEncoding($char, self::ENCODING, 'UCS-4BE'); + } + } + + /** + * Convert a string from one encoding to another. + * + * @param string $value The string to convert + * @param string $to The input encoding + * @param string $from The output encoding + * + * @return string The string with the new encoding + * + * @throws \RuntimeException if no suitable encoding function is found (iconv or mbstring) + */ + private function convertEncoding($value, $to, $from) + { + if (function_exists('mb_convert_encoding')) { + return mb_convert_encoding($value, $to, $from); + } elseif (function_exists('iconv')) { + return iconv($from, $to, $value); + } + + throw new \RuntimeException('No suitable convert encoding function (install the iconv or mbstring extension).'); + } +} + + * + * 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 + * + * @api + */ +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 + * + * @api + */ +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; + +if (!defined('JSON_UNESCAPED_UNICODE')) { + define('JSON_UNESCAPED_SLASHES', 64); + define('JSON_UNESCAPED_UNICODE', 256); +} + +/** + * Exception class thrown when an error occurs during parsing. + * + * @author Fabien Potencier + * + * @api + */ +class ParseException extends RuntimeException +{ + private $parsedFile; + private $parsedLine; + private $snippet; + private $rawMessage; + + /** + * Constructor. + * + * @param string $message The error message + * @param integer $parsedLine The line where the error occurred + * @param integer $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 integer The file line + */ + public function getParsedLine() + { + return $this->parsedLine; + } + + /** + * Sets the line where the error occurred. + * + * @param integer $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) { + $this->message .= sprintf(' in %s', json_encode($this->parsedFile, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + + 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 + * + * @api + */ +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; + + /** + * Converts a YAML string to a PHP array. + * + * @param string $value A YAML string + * @param Boolean $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param Boolean $objectSupport true if object support is enabled, false otherwise + * + * @return array A PHP array representing the YAML string + * + * @throws ParseException + */ + public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false) + { + self::$exceptionOnInvalidType = $exceptionOnInvalidType; + self::$objectSupport = $objectSupport; + + $value = trim($value); + + if (0 == strlen($value)) { + return ''; + } + + if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) { + $mbEncoding = mb_internal_encoding(); + mb_internal_encoding('ASCII'); + } + + $i = 0; + switch ($value[0]) { + case '[': + $result = self::parseSequence($value, $i); + ++$i; + break; + case '{': + $result = self::parseMapping($value, $i); + ++$i; + break; + default: + $result = self::parseScalar($value, null, array('"', "'"), $i); + } + + // 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 Boolean $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param Boolean $objectSupport true if object support is enabled, false otherwise + * + * @return string The YAML string representing the PHP array + * + * @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'); + } + $repr = is_string($value) ? "'$value'" : (is_infinite($value) ? str_ireplace('INF', '.Inf', strval($value)) : strval($value)); + + if (false !== $locale) { + setlocale(LC_NUMERIC, $locale); + } + + return $repr; + case Escaper::requiresDoubleQuoting($value): + return Escaper::escapeWithDoubleQuotes($value); + case Escaper::requiresSingleQuoting($value): + return Escaper::escapeWithSingleQuotes($value); + case '' == $value: + return "''"; + case preg_match(self::getTimestampRegex(), $value): + case in_array(strtolower($value), array('null', '~', 'true', 'false')): + return "'$value'"; + default: + return $value; + } + } + + /** + * Dumps a PHP array to a YAML string. + * + * @param array $value The PHP array to dump + * @param Boolean $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise + * @param Boolean $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 + $keys = array_keys($value); + if ((1 == count($keys) && '0' == $keys[0]) + || (count($keys) > 1 && array_reduce($keys, function ($v, $w) { return (integer) $v + $w; }, 0) == count($keys) * (count($keys) - 1) / 2) + ) { + $output = array(); + foreach ($value as $val) { + $output[] = self::dump($val, $exceptionOnInvalidType, $objectSupport); + } + + return sprintf('[%s]', implode(', ', $output)); + } + + // mapping + $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 scalar to a YAML string. + * + * @param scalar $scalar + * @param string $delimiters + * @param array $stringDelimiters + * @param integer &$i + * @param Boolean $evaluate + * + * @return string A YAML string + * + * @throws ParseException When malformed inline YAML string is parsed + */ + public static function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true) + { + 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 (false !== $strpos = strpos($output, ' #')) { + $output = rtrim(substr($output, 0, $strpos)); + } + } 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)); + } + + if ($evaluate) { + $output = self::evaluateScalar($output); + } + } + + return $output; + } + + /** + * Parses a quoted scalar to YAML. + * + * @param string $scalar + * @param integer &$i + * + * @return string A YAML 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 sequence to a YAML string. + * + * @param string $sequence + * @param integer &$i + * + * @return string A YAML string + * + * @throws ParseException When malformed inline YAML string is parsed + */ + private static function parseSequence($sequence, &$i = 0) + { + $output = array(); + $len = strlen($sequence); + $i += 1; + + // [foo, bar, ...] + while ($i < $len) { + switch ($sequence[$i]) { + case '[': + // nested sequence + $output[] = self::parseSequence($sequence, $i); + break; + case '{': + // nested mapping + $output[] = self::parseMapping($sequence, $i); + break; + case ']': + return $output; + case ',': + case ' ': + break; + default: + $isQuoted = in_array($sequence[$i], array('"', "'")); + $value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i); + + if (!$isQuoted && false !== strpos($value, ': ')) { + // embedded mapping? + try { + $value = self::parseMapping('{'.$value.'}'); + } catch (\InvalidArgumentException $e) { + // no, it's not + } + } + + $output[] = $value; + + --$i; + } + + ++$i; + } + + throw new ParseException(sprintf('Malformed inline YAML string %s', $sequence)); + } + + /** + * Parses a mapping to a YAML string. + * + * @param string $mapping + * @param integer &$i + * + * @return string A YAML string + * + * @throws ParseException When malformed inline YAML string is parsed + */ + private static function parseMapping($mapping, &$i = 0) + { + $output = array(); + $len = strlen($mapping); + $i += 1; + + // {foo: bar, bar:foo, ...} + while ($i < $len) { + switch ($mapping[$i]) { + case ' ': + case ',': + ++$i; + continue 2; + case '}': + return $output; + } + + // key + $key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false); + + // value + $done = false; + while ($i < $len) { + switch ($mapping[$i]) { + case '[': + // nested sequence + $output[$key] = self::parseSequence($mapping, $i); + $done = true; + break; + case '{': + // nested mapping + $output[$key] = self::parseMapping($mapping, $i); + $done = true; + break; + case ':': + case ' ': + break; + default: + $output[$key] = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i); + $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 + * + * @return string A YAML string + */ + private static function evaluateScalar($scalar) + { + $scalar = trim($scalar); + $scalarLower = strtolower($scalar); + switch (true) { + case 'null' === $scalarLower: + case '' === $scalar: + case '~' === $scalar: + return null; + 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 intval(self::parseScalar(substr($scalar, 2))); + 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 null; + case ctype_digit($scalar): + $raw = $scalar; + $cast = intval($scalar); + + return '0' == $scalar[0] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw); + case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)): + $raw = $scalar; + $cast = intval($scalar); + + return '0' == $scalar[1] ? octdec($scalar) : (((string) $raw == (string) $cast) ? $cast : $raw); + case is_numeric($scalar): + return '0x' == $scalar[0].$scalar[1] ? hexdec($scalar) : floatval($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 floatval(str_replace(',', '', $scalar)); + case preg_match(self::getTimestampRegex(), $scalar): + return strtotime($scalar); + } + 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; + } +} + + * + * 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. + * + * @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. + * + * @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. + * + * @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\Formatter; + +/** + * Formatter class for console output. + * + * @author Konstantin Kudryashov + * + * @api + */ +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) + { + return preg_replace('/([^\\\\]?) FormatterStyle" instances + * + * @api + */ + public function __construct($decorated = false, array $styles = array()) + { + $this->decorated = (Boolean) $decorated; + + $this->setStyle('error', new OutputFormatterStyle('white', 'red')); + $this->setStyle('info', new OutputFormatterStyle('green')); + $this->setStyle('comment', new OutputFormatterStyle('yellow')); + $this->setStyle('question', new OutputFormatterStyle('black', 'cyan')); + + foreach ($styles as $name => $style) { + $this->setStyle($name, $style); + } + + $this->styleStack = new OutputFormatterStyleStack(); + } + + /** + * Sets the decorated flag. + * + * @param Boolean $decorated Whether to decorate the messages or not + * + * @api + */ + public function setDecorated($decorated) + { + $this->decorated = (Boolean) $decorated; + } + + /** + * Gets the decorated flag. + * + * @return Boolean true if the output will decorate messages, false otherwise + * + * @api + */ + public function isDecorated() + { + return $this->decorated; + } + + /** + * Sets a new style. + * + * @param string $name The style name + * @param OutputFormatterStyleInterface $style The style instance + * + * @api + */ + public function setStyle($name, OutputFormatterStyleInterface $style) + { + $this->styles[strtolower($name)] = $style; + } + + /** + * Checks if output formatter has style with specified name. + * + * @param string $name + * + * @return Boolean + * + * @api + */ + public function hasStyle($name) + { + return isset($this->styles[strtolower($name)]); + } + + /** + * Gets style options from style with specified name. + * + * @param string $name + * + * @return OutputFormatterStyleInterface + * + * @throws \InvalidArgumentException When style isn't defined + * + * @api + */ + public function getStyle($name) + { + if (!$this->hasStyle($name)) { + throw new \InvalidArgumentException(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 + * + * @api + */ + public function format($message) + { + $offset = 0; + $output = ''; + $tagRegex = '[a-z][a-z0-9_=;-]*'; + preg_match_all("#<(($tagRegex) | /($tagRegex)?)>#isx", $message, $matches, PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $text = $match[0]; + + // 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 ($pos && '\\' == $message[$pos - 1]) { + // escaped tag + $output .= $this->applyCurrentStyle($text); + } 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)); + + 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|Boolean 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 { + $style->setOption($match[1]); + } + } + + 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 + * + * @api + */ +interface OutputFormatterInterface +{ + /** + * Sets the decorated flag. + * + * @param Boolean $decorated Whether to decorate the messages or not + * + * @api + */ + public function setDecorated($decorated); + + /** + * Gets the decorated flag. + * + * @return Boolean true if the output will decorate messages, false otherwise + * + * @api + */ + public function isDecorated(); + + /** + * Sets a new style. + * + * @param string $name The style name + * @param OutputFormatterStyleInterface $style The style instance + * + * @api + */ + public function setStyle($name, OutputFormatterStyleInterface $style); + + /** + * Checks if output formatter has style with specified name. + * + * @param string $name + * + * @return Boolean + * + * @api + */ + public function hasStyle($name); + + /** + * Gets style options from style with specified name. + * + * @param string $name + * + * @return OutputFormatterStyleInterface + * + * @api + */ + public function getStyle($name); + + /** + * Formats a message according to the given styles. + * + * @param string $message The message to style + * + * @return string The styled message + * + * @api + */ + public function format($message); +} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * Formatter style class for defining styles. + * + * @author Konstantin Kudryashov + * + * @api + */ +class OutputFormatterStyle implements OutputFormatterStyleInterface +{ + private static $availableForegroundColors = array( + 'black' => 30, + 'red' => 31, + 'green' => 32, + 'yellow' => 33, + 'blue' => 34, + 'magenta' => 35, + 'cyan' => 36, + 'white' => 37 + ); + private static $availableBackgroundColors = array( + 'black' => 40, + 'red' => 41, + 'green' => 42, + 'yellow' => 43, + 'blue' => 44, + 'magenta' => 45, + 'cyan' => 46, + 'white' => 47 + ); + private static $availableOptions = array( + 'bold' => 1, + 'underscore' => 4, + 'blink' => 5, + 'reverse' => 7, + 'conceal' => 8 + ); + + private $foreground; + private $background; + private $options = array(); + + /** + * Initializes output formatter style. + * + * @param string|null $foreground The style foreground color name + * @param string|null $background The style background color name + * @param array $options The style options + * + * @api + */ + public function __construct($foreground = null, $background = null, array $options = array()) + { + if (null !== $foreground) { + $this->setForeground($foreground); + } + if (null !== $background) { + $this->setBackground($background); + } + if (count($options)) { + $this->setOptions($options); + } + } + + /** + * Sets style foreground color. + * + * @param string|null $color The color name + * + * @throws \InvalidArgumentException When the color name isn't defined + * + * @api + */ + public function setForeground($color = null) + { + if (null === $color) { + $this->foreground = null; + + return; + } + + if (!isset(static::$availableForegroundColors[$color])) { + throw new \InvalidArgumentException(sprintf( + 'Invalid foreground color specified: "%s". Expected one of (%s)', + $color, + implode(', ', array_keys(static::$availableForegroundColors)) + )); + } + + $this->foreground = static::$availableForegroundColors[$color]; + } + + /** + * Sets style background color. + * + * @param string|null $color The color name + * + * @throws \InvalidArgumentException When the color name isn't defined + * + * @api + */ + public function setBackground($color = null) + { + if (null === $color) { + $this->background = null; + + return; + } + + if (!isset(static::$availableBackgroundColors[$color])) { + throw new \InvalidArgumentException(sprintf( + 'Invalid background color specified: "%s". Expected one of (%s)', + $color, + implode(', ', array_keys(static::$availableBackgroundColors)) + )); + } + + $this->background = static::$availableBackgroundColors[$color]; + } + + /** + * Sets some specific style option. + * + * @param string $option The option name + * + * @throws \InvalidArgumentException When the option name isn't defined + * + * @api + */ + public function setOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf( + 'Invalid option specified: "%s". Expected one of (%s)', + $option, + implode(', ', array_keys(static::$availableOptions)) + )); + } + + if (false === array_search(static::$availableOptions[$option], $this->options)) { + $this->options[] = static::$availableOptions[$option]; + } + } + + /** + * Unsets some specific style option. + * + * @param string $option The option name + * + * @throws \InvalidArgumentException When the option name isn't defined + * + */ + public function unsetOption($option) + { + if (!isset(static::$availableOptions[$option])) { + throw new \InvalidArgumentException(sprintf( + 'Invalid option specified: "%s". Expected one of (%s)', + $option, + implode(', ', array_keys(static::$availableOptions)) + )); + } + + $pos = array_search(static::$availableOptions[$option], $this->options); + if (false !== $pos) { + unset($this->options[$pos]); + } + } + + /** + * Sets multiple style options at once. + * + * @param array $options + */ + public function setOptions(array $options) + { + $this->options = array(); + + foreach ($options as $option) { + $this->setOption($option); + } + } + + /** + * Applies the style to a given text. + * + * @param string $text The text to style + * + * @return string + */ + public function apply($text) + { + $codes = array(); + + if (null !== $this->foreground) { + $codes[] = $this->foreground; + } + if (null !== $this->background) { + $codes[] = $this->background; + } + if (count($this->options)) { + $codes = array_merge($codes, $this->options); + } + + if (0 === count($codes)) { + return $text; + } + + return sprintf("\033[%sm%s\033[0m", implode(';', $codes), $text); + } +} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * Formatter style interface for defining styles. + * + * @author Konstantin Kudryashov + * + * @api + */ +interface OutputFormatterStyleInterface +{ + /** + * Sets style foreground color. + * + * @param string $color The color name + * + * @api + */ + public function setForeground($color = null); + + /** + * Sets style background color. + * + * @param string $color The color name + * + * @api + */ + public function setBackground($color = null); + + /** + * Sets some specific style option. + * + * @param string $option The option name + * + * @api + */ + public function setOption($option); + + /** + * Unsets some specific style option. + * + * @param string $option The option name + */ + public function unsetOption($option); + + /** + * Sets multiple style options at once. + * + * @param array $options + */ + public function setOptions(array $options); + + /** + * Applies the style to a given text. + * + * @param string $text The text to style + * + * @return string + */ + public function apply($text); +} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * @author Jean-François Simon + */ +class OutputFormatterStyleStack +{ + /** + * @var OutputFormatterStyleInterface[] + */ + private $styles; + + /** + * @var OutputFormatterStyleInterface + */ + private $emptyStyle; + + /** + * Constructor. + * + * @param OutputFormatterStyleInterface|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; + +/** + * HelperInterface is the interface all helpers must implement. + * + * @author Fabien Potencier + * + * @api + */ +interface HelperInterface +{ + /** + * Sets the helper set associated with this helper. + * + * @param HelperSet $helperSet A HelperSet instance + * + * @api + */ + public function setHelperSet(HelperSet $helperSet = null); + + /** + * Gets the helper set associated with this helper. + * + * @return HelperSet A HelperSet instance + * + * @api + */ + public function getHelperSet(); + + /** + * Returns the canonical name of this helper. + * + * @return string The canonical name + * + * @api + */ + 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\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 Boolean $large Whether to return a large block + * + * @return string The formatter message + */ + public function formatBlock($messages, $style, $large = false) + { + $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(); + foreach ($lines as $line) { + $messages[] = $line.str_repeat(' ', $len - $this->strlen($line)); + } + if ($large) { + $messages[] = str_repeat(' ', $len); + } + + foreach ($messages as &$message) { + $message = sprintf('<%s>%s', $style, $message, $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\Output\OutputInterface; +use InvalidArgumentException; + +/** + * Provides helpers to display table output. + * + * @author Саша Стаменковић + */ +class TableHelper extends Helper +{ + const LAYOUT_DEFAULT = 0; + const LAYOUT_BORDERLESS = 1; + const LAYOUT_COMPACT = 2; + + /** + * Table headers. + * + * @var array + */ + private $headers = array(); + + /** + * Table rows. + * + * @var array + */ + private $rows = array(); + + // Rendering options + private $paddingChar; + private $horizontalBorderChar; + private $verticalBorderChar; + private $crossingChar; + private $cellHeaderFormat; + private $cellRowFormat; + private $cellRowContentFormat; + private $borderFormat; + private $padType; + + /** + * Column widths cache. + * + * @var array + */ + private $columnWidths = array(); + + /** + * Number of columns cache. + * + * @var array + */ + private $numberOfColumns; + + /** + * @var OutputInterface + */ + private $output; + + public function __construct() + { + $this->setLayout(self::LAYOUT_DEFAULT); + } + + /** + * Sets table layout type. + * + * @param int $layout self::LAYOUT_* + * + * @return TableHelper + */ + public function setLayout($layout) + { + switch ($layout) { + case self::LAYOUT_BORDERLESS: + $this + ->setPaddingChar(' ') + ->setHorizontalBorderChar('=') + ->setVerticalBorderChar(' ') + ->setCrossingChar(' ') + ->setCellHeaderFormat('%s') + ->setCellRowFormat('%s') + ->setCellRowContentFormat(' %s ') + ->setBorderFormat('%s') + ->setPadType(STR_PAD_RIGHT) + ; + break; + + case self::LAYOUT_COMPACT: + $this + ->setPaddingChar(' ') + ->setHorizontalBorderChar('') + ->setVerticalBorderChar(' ') + ->setCrossingChar('') + ->setCellHeaderFormat('%s') + ->setCellRowFormat('%s') + ->setCellRowContentFormat('%s') + ->setBorderFormat('%s') + ->setPadType(STR_PAD_RIGHT) + ; + break; + + case self::LAYOUT_DEFAULT: + $this + ->setPaddingChar(' ') + ->setHorizontalBorderChar('-') + ->setVerticalBorderChar('|') + ->setCrossingChar('+') + ->setCellHeaderFormat('%s') + ->setCellRowFormat('%s') + ->setCellRowContentFormat(' %s ') + ->setBorderFormat('%s') + ->setPadType(STR_PAD_RIGHT) + ; + break; + + default: + throw new InvalidArgumentException(sprintf('Invalid table layout "%s".', $layout)); + break; + }; + + return $this; + } + + public function setHeaders(array $headers) + { + $this->headers = array_values($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(array $row) + { + $this->rows[] = array_values($row); + + $keys = array_keys($this->rows); + $rowKey = array_pop($keys); + + foreach ($row as $key => $cellValue) { + if (!strstr($cellValue, "\n")) { + continue; + } + + $lines = explode("\n", $cellValue); + $this->rows[$rowKey][$key] = $lines[0]; + unset($lines[0]); + + foreach ($lines as $lineKey => $line) { + $nextRowKey = $rowKey + $lineKey + 1; + + if (isset($this->rows[$nextRowKey])) { + $this->rows[$nextRowKey][$key] = $line; + } else { + $this->rows[$nextRowKey] = array($key => $line); + } + } + } + + return $this; + } + + public function setRow($column, array $row) + { + $this->rows[$column] = $row; + + return $this; + } + + /** + * Sets padding character, used for cell padding. + * + * @param string $paddingChar + * + * @return TableHelper + */ + public function setPaddingChar($paddingChar) + { + if (!$paddingChar) { + throw new \LogicException('The padding char must not be empty'); + } + + $this->paddingChar = $paddingChar; + + return $this; + } + + /** + * Sets horizontal border character. + * + * @param string $horizontalBorderChar + * + * @return TableHelper + */ + public function setHorizontalBorderChar($horizontalBorderChar) + { + $this->horizontalBorderChar = $horizontalBorderChar; + + return $this; + } + + /** + * Sets vertical border character. + * + * @param string $verticalBorderChar + * + * @return TableHelper + */ + public function setVerticalBorderChar($verticalBorderChar) + { + $this->verticalBorderChar = $verticalBorderChar; + + return $this; + } + + /** + * Sets crossing character. + * + * @param string $crossingChar + * + * @return TableHelper + */ + public function setCrossingChar($crossingChar) + { + $this->crossingChar = $crossingChar; + + return $this; + } + + /** + * Sets header cell format. + * + * @param string $cellHeaderFormat + * + * @return TableHelper + */ + public function setCellHeaderFormat($cellHeaderFormat) + { + $this->cellHeaderFormat = $cellHeaderFormat; + + return $this; + } + + /** + * Sets row cell format. + * + * @param string $cellRowFormat + * + * @return TableHelper + */ + public function setCellRowFormat($cellRowFormat) + { + $this->cellRowFormat = $cellRowFormat; + + return $this; + } + + /** + * Sets row cell content format. + * + * @param string $cellRowContentFormat + * + * @return TableHelper + */ + public function setCellRowContentFormat($cellRowContentFormat) + { + $this->cellRowContentFormat = $cellRowContentFormat; + + return $this; + } + + /** + * Sets table border format. + * + * @param string $borderFormat + * + * @return TableHelper + */ + public function setBorderFormat($borderFormat) + { + $this->borderFormat = $borderFormat; + + return $this; + } + + /** + * Sets cell padding type. + * + * @param integer $padType STR_PAD_* + * + * @return TableHelper + */ + public function setPadType($padType) + { + $this->padType = $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) + { + $this->output = $output; + + $this->renderRowSeparator(); + $this->renderRow($this->headers, $this->cellHeaderFormat); + if (!empty($this->headers)) { + $this->renderRowSeparator(); + } + foreach ($this->rows as $row) { + $this->renderRow($row, $this->cellRowFormat); + } + if (!empty($this->rows)) { + $this->renderRowSeparator(); + } + + $this->cleanup(); + } + + /** + * Renders horizontal header separator. + * + * Example: +-----+-----------+-------+ + */ + private function renderRowSeparator() + { + if (0 === $count = $this->getNumberOfColumns()) { + return; + } + + if (!$this->horizontalBorderChar && !$this->crossingChar) { + return; + } + + $markup = $this->crossingChar; + for ($column = 0; $column < $count; $column++) { + $markup .= str_repeat($this->horizontalBorderChar, $this->getColumnWidth($column)).$this->crossingChar; + } + + $this->output->writeln(sprintf($this->borderFormat, $markup)); + } + + /** + * Renders vertical column separator. + */ + private function renderColumnSeparator() + { + $this->output->write(sprintf($this->borderFormat, $this->verticalBorderChar)); + } + + /** + * 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; + } + + $this->renderColumnSeparator(); + for ($column = 0, $count = $this->getNumberOfColumns(); $column < $count; $column++) { + $this->renderCell($row, $column, $cellFormat); + $this->renderColumnSeparator(); + } + $this->output->writeln(''); + } + + /** + * Renders table cell with padding. + * + * @param array $row + * @param integer $column + * @param string $cellFormat + */ + private function renderCell(array $row, $column, $cellFormat) + { + $cell = isset($row[$column]) ? $row[$column] : ''; + $width = $this->getColumnWidth($column); + + // str_pad won't work properly with multi-byte strings, we need to fix the padding + if (function_exists('mb_strlen') && false !== $encoding = mb_detect_encoding($cell)) { + $width += strlen($cell) - mb_strlen($cell, $encoding); + } + + $width += $this->strlen($cell) - $this->computeLengthWithoutDecoration($cell); + + $content = sprintf($this->cellRowContentFormat, $cell); + + $this->output->write(sprintf($cellFormat, str_pad($content, $width, $this->paddingChar, $this->padType))); + } + + /** + * Gets number of columns for this table. + * + * @return int + */ + private function getNumberOfColumns() + { + if (null !== $this->numberOfColumns) { + return $this->numberOfColumns; + } + + $columns = array(0); + $columns[] = count($this->headers); + foreach ($this->rows as $row) { + $columns[] = count($row); + } + + return $this->numberOfColumns = max($columns); + } + + /** + * Gets column width. + * + * @param integer $column + * + * @return int + */ + private function getColumnWidth($column) + { + if (isset($this->columnWidths[$column])) { + return $this->columnWidths[$column]; + } + + $lengths = array(0); + $lengths[] = $this->getCellWidth($this->headers, $column); + foreach ($this->rows as $row) { + $lengths[] = $this->getCellWidth($row, $column); + } + + return $this->columnWidths[$column] = max($lengths) + strlen($this->cellRowContentFormat) - 2; + } + + /** + * Gets cell width. + * + * @param array $row + * @param integer $column + * + * @return int + */ + private function getCellWidth(array $row, $column) + { + return isset($row[$column]) ? $this->computeLengthWithoutDecoration($row[$column]) : 0; + } + + /** + * Called after rendering to cleanup cache data. + */ + private function cleanup() + { + $this->columnWidths = array(); + $this->numberOfColumns = null; + } + + private function computeLengthWithoutDecoration($string) + { + $formatter = $this->output->getFormatter(); + $isDecorated = $formatter->isDecorated(); + $formatter->setDecorated(false); + + $string = $formatter->format($string); + $formatter->setDecorated($isDecorated); + + return $this->strlen($string); + } + + /** + * {@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; + +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; + +/** + * 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 + */ + 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\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\OutputInterface; + +/** + * The Progress class provides helpers to display progress output. + * + * @author Chris Jones + * @author Fabien Potencier + */ +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 integer + */ + private $current; + + /** + * Maximum number of steps + * + * @var integer + */ + private $max; + + /** + * Start time of the progress bar + * + * @var integer + */ + 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), + ); + + /** + * 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 integer|null $max Maximum steps + */ + public function start(OutputInterface $output, $max = null) + { + $this->startTime = time(); + $this->current = 0; + $this->max = (int) $max; + $this->output = $output; + $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 integer $step Number of steps to advance + * @param Boolean $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 integer $current The current progress + * @param Boolean $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 = intval($this->current / $this->redrawFreq); + + $this->current = $current; + + $currPeriod = intval($this->current / $this->redrawFreq); + if ($redraw || $prevPeriod !== $currPeriod || $this->max === $this->current) { + $this->display(); + } + } + + /** + * Outputs the current progress string. + * + * @param Boolean $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 Boolean $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'])) { + $completeBars = 0; + + 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 integer $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\Output\OutputInterface; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; + +/** + * The Dialog class provides helpers to interact with the user. + * + * @author Fabien Potencier + */ +class DialogHelper extends InputAwareHelper +{ + private $inputStream; + private static $shell; + private static $stty; + + /** + * Asks the user to select a value. + * + * @param OutputInterface $output An Output instance + * @param string|array $question The question to ask + * @param array $choices List of choices to pick from + * @param Boolean|string $default The default answer if the user enters nothing + * @param Boolean|integer $attempts Max number of times to ask before giving up (false by default, which means infinite) + * @param string $errorMessage Message which will be shown if invalid value from choice list would be picked + * @param Boolean $multiselect Select more than one value separated by comma + * + * @return integer|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) + { + $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)); + } + array_push($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; + } + + $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 ('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 Boolean $default The default answer if the user enters nothing + * + * @return Boolean 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 Boolean $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 (defined('PHP_WINDOWS_VERSION_BUILD')) { + $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 integer $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 integer $attempts Max number of times to ask before giving up (false by default, which means infinite) + * @param Boolean $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 string + */ + public function getInputStream() + { + return $this->inputStream; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'dialog'; + } + + /** + * Return a valid Unix shell + * + * @return string|Boolean 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 integer $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) + { + $error = null; + while (false === $attempts || $attempts--) { + if (null !== $error) { + $output->writeln($this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error')); + } + + try { + return call_user_func($validator, $interviewer()); + } catch (\Exception $error) { + } + } + + throw $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; + +/** + * 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_strlen if it is available. + * + * @param string $string The string to check its length + * + * @return integer The length of the string + */ + protected function strlen($string) + { + if (!function_exists('mb_strlen')) { + return strlen($string); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return strlen($string); + } + + return mb_strlen($string, $encoding); + } +} + + * + * 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; + +/** + * HelperSet represents a set of helpers to be used with a command. + * + * @author Fabien Potencier + */ +class HelperSet implements \IteratorAggregate +{ + 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 Boolean 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)); + } + + 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; + } + + 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\Tester; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\StreamOutput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Eases the testing of console commands. + * + * @author Fabien Potencier + */ +class CommandTester +{ + private $command; + private $input; + private $output; + private $statusCode; + + /** + * Constructor. + * + * @param Command $command A Command instance to test. + */ + public function __construct(Command $command) + { + $this->command = $command; + } + + /** + * Executes the command. + * + * Available options: + * + * * interactive: Sets the input interactive flag + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * + * @param array $input An array of arguments and options + * @param array $options An array of options + * + * @return integer The command exit code + */ + public function execute(array $input, array $options = array()) + { + // set the command name automatically if the application requires + // this argument and no command name was passed + if (!isset($input['command']) + && (null !== $application = $this->command->getApplication()) + && $application->getDefinition()->hasArgument('command') + ) { + $input['command'] = $this->command->getName(); + } + + $this->input = new ArrayInput($input); + if (isset($options['interactive'])) { + $this->input->setInteractive($options['interactive']); + } + + $this->output = new StreamOutput(fopen('php://memory', 'w', false)); + if (isset($options['decorated'])) { + $this->output->setDecorated($options['decorated']); + } + if (isset($options['verbosity'])) { + $this->output->setVerbosity($options['verbosity']); + } + + return $this->statusCode = $this->command->run($this->input, $this->output); + } + + /** + * Gets the display returned by the last execution of the command. + * + * @param Boolean $normalize Whether to normalize end of lines to \n or not + * + * @return string The display + */ + public function getDisplay($normalize = false) + { + rewind($this->output->getStream()); + + $display = stream_get_contents($this->output->getStream()); + + if ($normalize) { + $display = str_replace(PHP_EOL, "\n", $display); + } + + return $display; + } + + /** + * Gets the input instance used by the last execution of the command. + * + * @return InputInterface The current input instance + */ + public function getInput() + { + return $this->input; + } + + /** + * Gets the output instance used by the last execution of the command. + * + * @return OutputInterface The current output instance + */ + public function getOutput() + { + return $this->output; + } + + /** + * Gets the status code returned by the last execution of the application. + * + * @return integer The status code + */ + public function getStatusCode() + { + return $this->statusCode; + } +} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\StreamOutput; + +/** + * Eases the testing of console applications. + * + * When testing an application, don't forget to disable the auto exit flag: + * + * $application = new Application(); + * $application->setAutoExit(false); + * + * @author Fabien Potencier + */ +class ApplicationTester +{ + private $application; + private $input; + private $output; + private $statusCode; + + /** + * Constructor. + * + * @param Application $application An Application instance to test. + */ + public function __construct(Application $application) + { + $this->application = $application; + } + + /** + * Executes the application. + * + * Available options: + * + * * interactive: Sets the input interactive flag + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * + * @param array $input An array of arguments and options + * @param array $options An array of options + * + * @return integer The command exit code + */ + public function run(array $input, $options = array()) + { + $this->input = new ArrayInput($input); + if (isset($options['interactive'])) { + $this->input->setInteractive($options['interactive']); + } + + $this->output = new StreamOutput(fopen('php://memory', 'w', false)); + if (isset($options['decorated'])) { + $this->output->setDecorated($options['decorated']); + } + if (isset($options['verbosity'])) { + $this->output->setVerbosity($options['verbosity']); + } + + return $this->statusCode = $this->application->run($this->input, $this->output); + } + + /** + * Gets the display returned by the last execution of the application. + * + * @param Boolean $normalize Whether to normalize end of lines to \n or not + * + * @return string The display + */ + public function getDisplay($normalize = false) + { + rewind($this->output->getStream()); + + $display = stream_get_contents($this->output->getStream()); + + if ($normalize) { + $display = str_replace(PHP_EOL, "\n", $display); + } + + return $display; + } + + /** + * Gets the input instance used by the last execution of the application. + * + * @return InputInterface The current input instance + */ + public function getInput() + { + return $this->input; + } + + /** + * Gets the output instance used by the last execution of the application. + * + * @return OutputInterface The current output instance + */ + public function getOutput() + { + return $this->output; + } + + /** + * Gets the status code returned by the last execution of the application. + * + * @return integer The status code + */ + public function getStatusCode() + { + return $this->statusCode; + } +} + + * + * 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\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 + * + * @api + */ +abstract class Output implements OutputInterface +{ + private $verbosity; + private $formatter; + + /** + * Constructor. + * + * @param integer $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) + * @param Boolean $decorated Whether to decorate messages + * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) + * + * @api + */ + 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; + } + + public function isQuiet() + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + public function isVerbose() + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + public function isVeryVerbose() + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + public function isDebug() + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + + /** + * {@inheritdoc} + */ + public function writeln($messages, $type = self::OUTPUT_NORMAL) + { + $this->write($messages, true, $type); + } + + /** + * {@inheritdoc} + */ + public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL) + { + if (self::VERBOSITY_QUIET === $this->verbosity) { + return; + } + + $messages = (array) $messages; + + 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; + default: + throw new \InvalidArgumentException(sprintf('Unknown output type given (%s)', $type)); + } + + $this->doWrite($message, $newline); + } + } + + /** + * Writes a message to the output. + * + * @param string $message A message to write to the output + * @param Boolean $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; + +/** + * 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 + * + * @api + */ +class StreamOutput extends Output +{ + private $stream; + + /** + * Constructor. + * + * @param mixed $stream A stream resource + * @param integer $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) + * @param Boolean|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 + * + * @api + */ + 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 ? PHP_EOL : ''))) { + // @codeCoverageIgnoreStart + // should never happen + throw new \RuntimeException('Unable to write output.'); + // @codeCoverageIgnoreEnd + } + + fflush($this->stream); + } + + /** + * Returns true if the stream supports colorization. + * + * Colorization is disabled if not supported by the stream: + * + * - Windows without Ansicon and ConEmu + * - non tty consoles + * + * @return Boolean true if the stream supports colorization, false otherwise + */ + protected function hasColorSupport() + { + // @codeCoverageIgnoreStart + if (DIRECTORY_SEPARATOR == '\\') { + return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI'); + } + + return function_exists('posix_isatty') && @posix_isatty($this->stream); + // @codeCoverageIgnoreEnd + } +} + + * + * 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 + * + * @api + */ +class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface +{ + private $stderr; + + /** + * Constructor. + * + * @param integer $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) + * @param Boolean|null $decorated Whether to decorate messages (null for auto-guessing) + * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) + * + * @api + */ + public function __construct($verbosity = self::VERBOSITY_NORMAL, $decorated = null, OutputFormatterInterface $formatter = null) + { + $outputStream = 'php://stdout'; + if (!$this->hasStdoutSupport()) { + $outputStream = 'php://output'; + } + + parent::__construct(fopen($outputStream, 'w'), $verbosity, $decorated, $formatter); + + $this->stderr = new StreamOutput(fopen('php://stderr', 'w'), $verbosity, $decorated, $formatter); + } + + /** + * {@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. + * + * IBM iSeries (OS400) exhibits character-encoding issues when writing to + * STDOUT and doesn't properly convert ASCII to EBCDIC, resulting in garbage + * output. + * + * @return boolean + */ + protected function hasStdoutSupport() + { + return ('OS400' != php_uname('s')); + } +} + + * + * 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 + * + * @api + */ +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 writeln($messages, $type = self::OUTPUT_NORMAL) + { + // do nothing + } + + /** + * {@inheritdoc} + */ + public function write($messages, $newline = false, $type = 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; + +/** + * @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; + +/** + * OutputInterface is the interface implemented by all Output classes. + * + * @author Fabien Potencier + * + * @api + */ +interface OutputInterface +{ + const VERBOSITY_QUIET = 0; + const VERBOSITY_NORMAL = 1; + const VERBOSITY_VERBOSE = 2; + const VERBOSITY_VERY_VERBOSE = 3; + const VERBOSITY_DEBUG = 4; + + const OUTPUT_NORMAL = 0; + const OUTPUT_RAW = 1; + const OUTPUT_PLAIN = 2; + + /** + * Writes a message to the output. + * + * @param string|array $messages The message as an array of lines or a single string + * @param Boolean $newline Whether to add a newline + * @param integer $type The type of output (one of the OUTPUT constants) + * + * @throws \InvalidArgumentException When unknown output type is given + * + * @api + */ + public function write($messages, $newline = false, $type = self::OUTPUT_NORMAL); + + /** + * 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 integer $type The type of output (one of the OUTPUT constants) + * + * @throws \InvalidArgumentException When unknown output type is given + * + * @api + */ + public function writeln($messages, $type = self::OUTPUT_NORMAL); + + /** + * Sets the verbosity of the output. + * + * @param integer $level The level of verbosity (one of the VERBOSITY constants) + * + * @api + */ + public function setVerbosity($level); + + /** + * Gets the current verbosity of the output. + * + * @return integer The current level of verbosity (one of the VERBOSITY constants) + * + * @api + */ + public function getVerbosity(); + + /** + * Sets the decorated flag. + * + * @param Boolean $decorated Whether to decorate the messages + * + * @api + */ + public function setDecorated($decorated); + + /** + * Gets the decorated flag. + * + * @return Boolean true if the output will decorate messages, false otherwise + * + * @api + */ + public function isDecorated(); + + /** + * Sets output formatter. + * + * @param OutputFormatterInterface $formatter + * + * @api + */ + public function setFormatter(OutputFormatterInterface $formatter); + + /** + * Returns current output formatter instance. + * + * @return OutputFormatterInterface + * + * @api + */ + 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\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; + +/** + * @author Jean-François Simon + */ +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 boolean $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\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 + */ +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($usageXML = $dom->createElement('usage')); + $usageXML->appendChild($dom->createTextNode(sprintf($command->getSynopsis(), ''))); + + $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()))); + + $commandXML->appendChild($aliasesXML = $dom->createElement('aliases')); + foreach ($command->getAliases() as $alias) { + $aliasesXML->appendChild($aliasXML = $dom->createElement('alias')); + $aliasXML->appendChild($dom->createTextNode($alias)); + } + + $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\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 + */ +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 = ''; + } + + $nameWidth = isset($options['name_width']) ? $options['name_width'] : strlen($argument->getName()); + + $this->writeText(sprintf(" %-${nameWidth}s %s%s", + $argument->getName(), + str_replace("\n", "\n".str_repeat(' ', $nameWidth + 2), $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 = ''; + } + + $nameWidth = isset($options['name_width']) ? $options['name_width'] : strlen($option->getName()); + $nameWithShortcutWidth = $nameWidth - strlen($option->getName()) - 2; + + $this->writeText(sprintf(" %s %-${nameWithShortcutWidth}s%s%s%s", + '--'.$option->getName(), + $option->getShortcut() ? sprintf('(-%s) ', $option->getShortcut()) : '', + str_replace("\n", "\n".str_repeat(' ', $nameWidth + 2), $option->getDescription()), + $default, + $option->isArray() ? ' (multiple values allowed)' : '' + ), $options); + } + + /** + * {@inheritdoc} + */ + protected function describeInputDefinition(InputDefinition $definition, array $options = array()) + { + $nameWidth = 0; + foreach ($definition->getOptions() as $option) { + $nameLength = strlen($option->getName()) + 2; + if ($option->getShortcut()) { + $nameLength += strlen($option->getShortcut()) + 3; + } + $nameWidth = max($nameWidth, $nameLength); + } + foreach ($definition->getArguments() as $argument) { + $nameWidth = max($nameWidth, strlen($argument->getName())); + } + ++$nameWidth; + + if ($definition->getArguments()) { + $this->writeText('Arguments:', $options); + $this->writeText("\n"); + foreach ($definition->getArguments() as $argument) { + $this->describeInputArgument($argument, array_merge($options, array('name_width' => $nameWidth))); + $this->writeText("\n"); + } + } + + if ($definition->getArguments() && $definition->getOptions()) { + $this->writeText("\n"); + } + + if ($definition->getOptions()) { + $this->writeText('Options:', $options); + $this->writeText("\n"); + foreach ($definition->getOptions() as $option) { + $this->describeInputOption($option, array_merge($options, array('name_width' => $nameWidth))); + $this->writeText("\n"); + } + } + } + + /** + * {@inheritdoc} + */ + protected function describeCommand(Command $command, array $options = array()) + { + $command->getSynopsis(); + $command->mergeApplicationDefinition(false); + + $this->writeText('Usage:', $options); + $this->writeText("\n"); + $this->writeText(' '.$command->getSynopsis(), $options); + $this->writeText("\n"); + + if (count($command->getAliases()) > 0) { + $this->writeText("\n"); + $this->writeText('Aliases: '.implode(', ', $command->getAliases()).'', $options); + } + + if ($definition = $command->getNativeDefinition()) { + $this->writeText("\n"); + $this->describeInputDefinition($definition, $options); + } + + $this->writeText("\n"); + + if ($help = $command->getProcessedHelp()) { + $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 { + $width = $this->getColumnWidth($description->getCommands()); + + $this->writeText($application->getHelp(), $options); + $this->writeText("\n\n"); + + 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"); + $this->writeText(sprintf(" %-${width}s %s", $name, $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 (version_compare(PHP_VERSION, '5.4', '<')) { + return str_replace('\/', '/', json_encode($default)); + } + + return json_encode($default, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + /** + * @param Command[] $commands + * + * @return int + */ + private function getColumnWidth(array $commands) + { + $width = 0; + foreach ($commands as $command) { + $width = strlen($command->getName()) > $width ? strlen($command->getName()) : $width; + } + + return $width + 2; + } +} + + * + * 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 + */ +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: '.($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: '.($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: `'.$command->getSynopsis().'`'."\n" + .'* Aliases: '.(count($command->getAliases()) ? '`'.implode('`, `', $command->getAliases()).'`' : '') + ); + + if ($help = $command->getProcessedHelp()) { + $this->write("\n\n"); + $this->write($help); + } + + if ($definition = $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; + +/** + * @author Jean-François Simon + */ +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 \InvalidArgumentException + */ + public function getCommand($name) + { + if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { + throw new \InvalidArgumentException(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(); + foreach ($commands as $name => $command) { + $key = $this->application->extractNamespace($name, 1); + if (!$key) { + $key = '_global'; + } + + $namespacedCommands[$key][$name] = $command; + } + ksort($namespacedCommands); + + foreach ($namespacedCommands as &$commands) { + ksort($commands); + } + + 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; + +/** + * JSON descriptor. + * + * @author Jean-François Simon + */ +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' => $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' => $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' => $command->getSynopsis(), + 'description' => $command->getDescription(), + 'help' => $command->getProcessedHelp(), + 'aliases' => $command->getAliases(), + '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\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\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 integer + */ + 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 integer $exitCode The command exit code + */ + public function setExitCode($exitCode) + { + $this->exitCode = (int) $exitCode; + } + + /** + * Gets the exit code. + * + * @return integer 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; +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; + +/** + * Allows to do things before the command is executed. + * + * @author Fabien Potencier + */ +class ConsoleCommandEvent extends ConsoleEvent +{ +} + + * + * 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 integer 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\Input; + +/** + * ArrayInput represents an input provided as an array. + * + * Usage: + * + * $input = new ArrayInput(array('name' => 'foo', '--bar' => 'foobar')); + * + * @author Fabien Potencier + * + * @api + */ +class ArrayInput extends Input +{ + private $parameters; + + /** + * Constructor. + * + * @param array $parameters An array of parameters + * @param InputDefinition $definition A InputDefinition instance + * + * @api + */ + public function __construct(array $parameters, InputDefinition $definition = null) + { + $this->parameters = $parameters; + + parent::__construct($definition); + } + + /** + * Returns the first argument from the raw parameters (not parsed). + * + * @return string The value of the first argument or null otherwise + */ + public function getFirstArgument() + { + foreach ($this->parameters as $key => $value) { + if ($key && '-' === $key[0]) { + continue; + } + + return $value; + } + } + + /** + * 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 Boolean true if the value is contained in the raw parameters + */ + 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; + } + + /** + * 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) + { + $values = (array) $values; + + foreach ($this->parameters as $k => $v) { + if (is_int($k) && 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); + } + + /** + * Processes command line arguments. + */ + 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 \InvalidArgumentException When option given doesn't exist + */ + private function addShortOption($shortcut, $value) + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new \InvalidArgumentException(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 \InvalidArgumentException When option given doesn't exist + * @throws \InvalidArgumentException When a required value is missing + */ + private function addLongOption($name, $value) + { + if (!$this->definition->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); + } + + $option = $this->definition->getOption($name); + + if (null === $value) { + if ($option->isValueRequired()) { + throw new \InvalidArgumentException(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; + +/** + * StringInput represents an input provided as a string. + * + * Usage: + * + * $input = new StringInput('foo --bar="foobar"'); + * + * @author Fabien Potencier + * + * @api + */ +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 + // @codeCoverageIgnoreStart + throw new \InvalidArgumentException(sprintf('Unable to parse input near "... %s ..."', substr($input, $cursor, 10))); + // @codeCoverageIgnoreEnd + } + + $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\Input; + +if (!defined('JSON_UNESCAPED_UNICODE')) { + define('JSON_UNESCAPED_SLASHES', 64); + define('JSON_UNESCAPED_UNICODE', 256); +} + +use Symfony\Component\Console\Descriptor\TextDescriptor; +use Symfony\Component\Console\Descriptor\XmlDescriptor; +use Symfony\Component\Console\Output\BufferedOutput; + +/** + * 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 + * + * @api + */ +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 + * + * @api + */ + public function __construct(array $definition = array()) + { + $this->setDefinition($definition); + } + + /** + * Sets the definition of the input. + * + * @param array $definition The definition array + * + * @api + */ + 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 + * + * @api + */ + 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 + * + * @api + */ + 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 + * + * @api + */ + 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|integer $name The InputArgument name or position + * + * @return InputArgument An InputArgument object + * + * @throws \InvalidArgumentException When argument given doesn't exist + * + * @api + */ + 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|integer $name The InputArgument name or position + * + * @return Boolean true if the InputArgument object exists, false otherwise + * + * @api + */ + 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 + * + * @api + */ + public function getArguments() + { + return $this->arguments; + } + + /** + * Returns the number of InputArguments. + * + * @return integer The number of InputArguments + */ + public function getArgumentCount() + { + return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments); + } + + /** + * Returns the number of required InputArguments. + * + * @return integer 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 + * + * @api + */ + 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 + * + * @api + */ + 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 + * + * @api + */ + 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 + * + * @api + */ + 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. + * + * @param string $name The InputOption name + * + * @return Boolean true if the InputOption object exists, false otherwise + * + * @api + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } + + /** + * Gets the array of InputOption objects. + * + * @return InputOption[] An array of InputOption objects + * + * @api + */ + public function getOptions() + { + return $this->options; + } + + /** + * Returns true if an InputOption object exists by shortcut. + * + * @param string $name The InputOption shortcut + * + * @return Boolean 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. + * + * @return string The synopsis + */ + public function getSynopsis() + { + $elements = array(); + foreach ($this->getOptions() as $option) { + $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; + $elements[] = sprintf('['.($option->isValueRequired() ? '%s--%s="..."' : ($option->isValueOptional() ? '%s--%s[="..."]' : '%s--%s')).']', $shortcut, $option->getName()); + } + + foreach ($this->getArguments() as $argument) { + $elements[] = sprintf($argument->isRequired() ? '%s' : '[%s]', $argument->getName().($argument->isArray() ? '1' : '')); + + if ($argument->isArray()) { + $elements[] = sprintf('... [%sN]', $argument->getName()); + } + } + + return implode(' ', $elements); + } + + /** + * Returns a textual representation of the InputDefinition. + * + * @return string A string representing the InputDefinition + * + * @deprecated Deprecated since version 2.3, to be removed in 3.0. + */ + public function asText() + { + $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 Boolean $asDom Whether to return a DOM or an XML string + * + * @return string|\DOMDocument An XML string representing the InputDefinition + * + * @deprecated Deprecated since version 2.3, to be removed in 3.0. + */ + public function asXml($asDom = false) + { + $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; + +/** + * 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 Boolean 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 if arguments given are correct. + * + * Throws an exception when not enough arguments are given. + * + * @throws \RuntimeException + */ + public function validate(); + + /** + * Returns all the given arguments merged with the default values. + * + * @return array + */ + public function getArguments(); + + /** + * Gets argument by name. + * + * @param string $name The name of the argument + * + * @return mixed + */ + 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|integer $name The InputArgument name or position + * + * @return Boolean 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(); + + /** + * Gets an option by name. + * + * @param string $name The name of the option + * + * @return mixed + */ + public function getOption($name); + + /** + * Sets an option value by name. + * + * @param string $name The option name + * @param string|boolean $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 Boolean true if the InputOption object exists, false otherwise + */ + public function hasOption($name); + + /** + * Is this input means interactive? + * + * @return Boolean + */ + public function isInteractive(); + + /** + * Sets the input interactivity. + * + * @param Boolean $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; + +/** + * Represents a command line argument. + * + * @author Fabien Potencier + * + * @api + */ +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 integer $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 + * + * @api + */ + 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 Boolean 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 Boolean 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; + +/** + * Represents a command line option. + * + * @author Fabien Potencier + * + * @api + */ +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 integer $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_REQUIRED or self::VALUE_NONE) + * + * @throws \InvalidArgumentException If option mode is invalid or incompatible + * + * @api + */ + 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 Boolean 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 Boolean 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 Boolean 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 Boolean 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 Boolean + */ + 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; + +/** + * 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 $definition A InputDefinition instance + */ + public function __construct(InputDefinition $definition = null) + { + if (null === $definition) { + $this->definition = new InputDefinition(); + } else { + $this->bind($definition); + $this->validate(); + } + } + + /** + * Binds the current Input instance with the given arguments and options. + * + * @param InputDefinition $definition A InputDefinition instance + */ + public function bind(InputDefinition $definition) + { + $this->arguments = array(); + $this->options = array(); + $this->definition = $definition; + + $this->parse(); + } + + /** + * Processes command line arguments. + */ + abstract protected function parse(); + + /** + * Validates the input. + * + * @throws \RuntimeException When not enough arguments are given + */ + public function validate() + { + if (count($this->arguments) < $this->definition->getArgumentRequiredCount()) { + throw new \RuntimeException('Not enough arguments.'); + } + } + + /** + * Checks if the input is interactive. + * + * @return Boolean Returns true if the input is interactive + */ + public function isInteractive() + { + return $this->interactive; + } + + /** + * Sets the input interactivity. + * + * @param Boolean $interactive If the input should be interactive + */ + public function setInteractive($interactive) + { + $this->interactive = (Boolean) $interactive; + } + + /** + * Returns the argument values. + * + * @return array An array of argument values + */ + public function getArguments() + { + return array_merge($this->definition->getArgumentDefaults(), $this->arguments); + } + + /** + * 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) + { + 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(); + } + + /** + * 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) + { + if (!$this->definition->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } + + /** + * Returns true if an InputArgument object exists by name or position. + * + * @param string|integer $name The InputArgument name or position + * + * @return Boolean true if the InputArgument object exists, false otherwise + */ + public function hasArgument($name) + { + return $this->definition->hasArgument($name); + } + + /** + * Returns the options values. + * + * @return array An array of option values + */ + public function getOptions() + { + return array_merge($this->definition->getOptionDefaults(), $this->options); + } + + /** + * 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) + { + 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(); + } + + /** + * Sets an option value by name. + * + * @param string $name The option name + * @param string|boolean $value The option value + * + * @throws \InvalidArgumentException When option given doesn't exist + */ + 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; + } + + /** + * Returns true if an InputOption object exists by name. + * + * @param string $name The InputOption name + * + * @return Boolean true if the InputOption object exists, false otherwise + */ + 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; + +/** + * 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 + * + * @api + */ +class ArgvInput extends Input +{ + private $tokens; + private $parsed; + + /** + * Constructor. + * + * @param array $argv An array of parameters from the CLI (in the argv format) + * @param InputDefinition $definition A InputDefinition instance + * + * @api + */ + 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; + } + + /** + * Processes command line arguments. + */ + 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, '=')) { + $this->addLongOption(substr($name, 0, $pos), substr($name, $pos + 1)); + } 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 { + throw new \RuntimeException('Too many arguments.'); + } + } + + /** + * 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 false values (from a previous call to substr()) to null + if (false === $value) { + $value = null; + } + + if (null !== $value && !$option->acceptValue()) { + throw new \RuntimeException(sprintf('The "--%s" option does not accept a value.', $name, $value)); + } + + 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 = ''; + } 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; + } + } + + /** + * Returns the first argument from the raw parameters (not parsed). + * + * @return string The value of the first argument or null otherwise + */ + public function getFirstArgument() + { + foreach ($this->tokens as $token) { + if ($token && '-' === $token[0]) { + continue; + } + + return $token; + } + } + + /** + * 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 value(s) to look for in the raw parameters (can be an array) + * + * @return Boolean true if the value is contained in the raw parameters + */ + 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; + } + + /** + * 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) + { + $values = (array) $values; + + $tokens = $this->tokens; + while ($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\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, 'To output help in other formats', 'txt'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), + )) + ->setDescription('Displays help for a command') + ->setHelp(<<%command.name% command displays help for a given command: + + php %command.full_name% list + +You can also output the help 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')) { + $input->setOption('format', 'xml'); + } + + $helper = new DescriptorHelper(); + $helper->describe($output, $this->command, array( + 'format' => $input->getOption('format'), + 'raw' => $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\Descriptor\TextDescriptor; +use Symfony\Component\Console\Descriptor\XmlDescriptor; +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; + +/** + * Base class for all commands. + * + * @author Fabien Potencier + * + * @api + */ +class Command +{ + private $application; + private $name; + private $aliases = array(); + private $definition; + private $help; + private $description; + private $ignoreValidationErrors = false; + private $applicationDefinitionMerged = false; + private $applicationDefinitionMergedWithArgs = false; + private $code; + private $synopsis; + 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 + * + * @api + */ + public function __construct($name = null) + { + $this->definition = new InputDefinition(); + + if (null !== $name) { + $this->setName($name); + } + + $this->configure(); + + if (!$this->name) { + throw new \LogicException('The command name cannot be empty.'); + } + } + + /** + * Ignores validation errors. + * + * This is mainly useful for the help command. + */ + public function ignoreValidationErrors() + { + $this->ignoreValidationErrors = true; + } + + /** + * Sets the application instance for this command. + * + * @param Application $application An Application instance + * + * @api + */ + public function setApplication(Application $application = null) + { + $this->application = $application; + if ($application) { + $this->setHelperSet($application->getHelperSet()); + } else { + $this->helperSet = null; + } + } + + /** + * Sets the helper set. + * + * @param HelperSet $helperSet A HelperSet instance + */ + public function setHelperSet(HelperSet $helperSet) + { + $this->helperSet = $helperSet; + } + + /** + * Gets the helper set. + * + * @return HelperSet A HelperSet instance + */ + public function getHelperSet() + { + return $this->helperSet; + } + + /** + * Gets the application instance for this command. + * + * @return Application An Application instance + * + * @api + */ + public function getApplication() + { + return $this->application; + } + + /** + * Checks whether the command is enabled or not in the current environment + * + * Override this to check for x or y and return false if the command can not + * run properly under the current conditions. + * + * @return Boolean + */ + public function isEnabled() + { + return true; + } + + /** + * Configures the current command. + */ + protected function configure() + { + } + + /** + * Executes the current command. + * + * This method is not abstract because you can use this class + * as a concrete class. In this case, instead of defining the + * execute() method, you set the code to execute by passing + * a Closure to the setCode() method. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + * + * @return null|integer null or 0 if everything went fine, or an error code + * + * @throws \LogicException When this abstract method is not implemented + * @see setCode() + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + throw new \LogicException('You must override the execute() method in the concrete command class.'); + } + + /** + * Interacts with the user. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + } + + /** + * Initializes the command just after the input has been validated. + * + * This is mainly useful when a lot of commands extends one main command + * where some things need to be initialized based on the input arguments and options. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + */ + protected function initialize(InputInterface $input, OutputInterface $output) + { + } + + /** + * Runs the command. + * + * The code to execute is either defined directly with the + * setCode() method or by overriding the execute() method + * in a sub-class. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + * + * @return integer The command exit code + * + * @throws \Exception + * + * @see setCode() + * @see execute() + * + * @api + */ + public function run(InputInterface $input, OutputInterface $output) + { + // force the creation of the synopsis before the merge with the app definition + $this->getSynopsis(); + + // add the application arguments and options + $this->mergeApplicationDefinition(); + + // bind the input against the command specific arguments/options + try { + $input->bind($this->definition); + } catch (\Exception $e) { + if (!$this->ignoreValidationErrors) { + throw $e; + } + } + + $this->initialize($input, $output); + + if ($input->isInteractive()) { + $this->interact($input, $output); + } + + $input->validate(); + + if ($this->code) { + $statusCode = call_user_func($this->code, $input, $output); + } else { + $statusCode = $this->execute($input, $output); + } + + return is_numeric($statusCode) ? (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() + * + * @api + */ + public function setCode($code) + { + if (!is_callable($code)) { + throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.'); + } + + $this->code = $code; + + return $this; + } + + /** + * Merges the application definition with the command definition. + * + * This method is not part of public API and should not be used directly. + * + * @param Boolean $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; + } + + if ($mergeArgs) { + $currentArguments = $this->definition->getArguments(); + $this->definition->setArguments($this->application->getDefinition()->getArguments()); + $this->definition->addArguments($currentArguments); + } + + $this->definition->addOptions($this->application->getDefinition()->getOptions()); + + $this->applicationDefinitionMerged = true; + 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 + * + * @api + */ + public function setDefinition($definition) + { + if ($definition instanceof InputDefinition) { + $this->definition = $definition; + } else { + $this->definition->setDefinition($definition); + } + + $this->applicationDefinitionMerged = false; + + return $this; + } + + /** + * Gets the InputDefinition attached to this Command. + * + * @return InputDefinition An InputDefinition instance + * + * @api + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Gets the InputDefinition to be used to create XML and Text representations of this Command. + * + * Can be overridden to provide the original command representation when it would otherwise + * be changed by merging with the application InputDefinition. + * + * 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 integer $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL + * @param string $description A description text + * @param mixed $default The default value (for InputArgument::OPTIONAL mode only) + * + * @return Command The current instance + * + * @api + */ + public function addArgument($name, $mode = null, $description = '', $default = null) + { + $this->definition->addArgument(new InputArgument($name, $mode, $description, $default)); + + return $this; + } + + /** + * Adds an option. + * + * @param string $name The option name + * @param string $shortcut The shortcut (can be null) + * @param integer $mode The option mode: One of the InputOption::VALUE_* constants + * @param string $description A description text + * @param mixed $default The default value (must be null for InputOption::VALUE_REQUIRED or InputOption::VALUE_NONE) + * + * @return Command The current instance + * + * @api + */ + public function addOption($name, $shortcut = null, $mode = null, $description = '', $default = null) + { + $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default)); + + return $this; + } + + /** + * Sets the name of the command. + * + * This method can set both the namespace and the name if + * you separate them by a colon (:) + * + * $command->setName('foo:bar'); + * + * @param string $name The command name + * + * @return Command The current instance + * + * @throws \InvalidArgumentException When the name is invalid + * + * @api + */ + public function setName($name) + { + $this->validateName($name); + + $this->name = $name; + + return $this; + } + + /** + * Returns the command name. + * + * @return string The command name + * + * @api + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the description for the command. + * + * @param string $description The description for the command + * + * @return Command The current instance + * + * @api + */ + public function setDescription($description) + { + $this->description = $description; + + return $this; + } + + /** + * Returns the description for the command. + * + * @return string The description for the command + * + * @api + */ + public function getDescription() + { + return $this->description; + } + + /** + * Sets the help for the command. + * + * @param string $help The help for the command + * + * @return Command The current instance + * + * @api + */ + public function setHelp($help) + { + $this->help = $help; + + return $this; + } + + /** + * Returns the help for the command. + * + * @return string The help for the command + * + * @api + */ + public function getHelp() + { + return $this->help; + } + + /** + * Returns the processed help for the command replacing the %command.name% and + * %command.full_name% patterns with the real values dynamically. + * + * @return string The processed help for the command + */ + public function getProcessedHelp() + { + $name = $this->name; + + $placeholders = array( + '%command.name%', + '%command.full_name%' + ); + $replacements = array( + $name, + $_SERVER['PHP_SELF'].' '.$name + ); + + return str_replace($placeholders, $replacements, $this->getHelp()); + } + + /** + * Sets the aliases for the command. + * + * @param array $aliases An array of aliases for the command + * + * @return Command The current instance + * + * @throws \InvalidArgumentException When an alias is invalid + * + * @api + */ + public function setAliases($aliases) + { + foreach ($aliases as $alias) { + $this->validateName($alias); + } + + $this->aliases = $aliases; + + return $this; + } + + /** + * Returns the aliases for the command. + * + * @return array An array of aliases for the command + * + * @api + */ + public function getAliases() + { + return $this->aliases; + } + + /** + * Returns the synopsis for the command. + * + * @return string The synopsis + */ + public function getSynopsis() + { + if (null === $this->synopsis) { + $this->synopsis = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis())); + } + + return $this->synopsis; + } + + /** + * Gets a helper instance by name. + * + * @param string $name The helper name + * + * @return mixed The helper value + * + * @throws \InvalidArgumentException if the helper is not defined + * + * @api + */ + public function getHelper($name) + { + return $this->helperSet->get($name); + } + + /** + * Returns a text representation of the command. + * + * @return string A string representing the command + * + * @deprecated Deprecated since version 2.3, to be removed in 3.0. + */ + public function asText() + { + $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 Boolean $asDom Whether to return a DOM or an XML string + * + * @return string|\DOMDocument An XML string representing the command + * + * @deprecated Deprecated since version 2.3, to be removed in 3.0. + */ + public function asXml($asDom = false) + { + $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; +use Symfony\Component\Console\Input\InputDefinition; + +/** + * ListCommand displays the list of all available commands for the application. + * + * @author Fabien Potencier + */ +class ListCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('list') + ->setDefinition($this->createDefinition()) + ->setDescription('Lists commands') + ->setHelp(<<%command.name% command lists all commands: + + php %command.full_name% + +You can also display the commands for a specific namespace: + + php %command.full_name% test + +You can also output the information 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')) { + $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, 'To output list in other formats', 'txt'), + )); + } +} + + * + * 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\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\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 + * + * @api + */ +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; + + /** + * Constructor. + * + * @param string $name The name of the application + * @param string $version The version of the application + * + * @api + */ + public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') + { + $this->name = $name; + $this->version = $version; + $this->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 integer 0 if everything went fine, or an error code + * + * @throws \Exception When doRun returns Exception + * + * @api + */ + public function run(InputInterface $input = null, OutputInterface $output = null) + { + if (null === $input) { + $input = new ArgvInput(); + } + + if (null === $output) { + $output = new ConsoleOutput(); + } + + $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; + } + // @codeCoverageIgnoreStart + exit($exitCode); + // @codeCoverageIgnoreEnd + } + + return $exitCode; + } + + /** + * Runs the current application. + * + * @param InputInterface $input An Input instance + * @param OutputInterface $output An Output instance + * + * @return integer 0 if everything went fine, or an error code + */ + public function doRun(InputInterface $input, OutputInterface $output) + { + 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 = 'list'; + $input = new ArrayInput(array('command' => 'list')); + } + + // the command name MUST be the first element of the input + $command = $this->find($name); + + $this->runningCommand = $command; + $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 + * + * @api + */ + public function setHelperSet(HelperSet $helperSet) + { + $this->helperSet = $helperSet; + } + + /** + * Get the helper set associated with the command. + * + * @return HelperSet The HelperSet instance associated with this command + * + * @api + */ + public function getHelperSet() + { + return $this->helperSet; + } + + /** + * Set an input definition set to be used with this application + * + * @param InputDefinition $definition The input definition + * + * @api + */ + public function setDefinition(InputDefinition $definition) + { + $this->definition = $definition; + } + + /** + * Gets the InputDefinition related to this Application. + * + * @return InputDefinition The InputDefinition instance + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Gets the help message. + * + * @return string A help message. + */ + public function getHelp() + { + $messages = array( + $this->getLongVersion(), + '', + 'Usage:', + ' [options] command [arguments]', + '', + 'Options:', + ); + + foreach ($this->getDefinition()->getOptions() as $option) { + $messages[] = sprintf(' %-29s %s %s', + '--'.$option->getName().'', + $option->getShortcut() ? '-'.$option->getShortcut().'' : ' ', + $option->getDescription() + ); + } + + return implode(PHP_EOL, $messages); + } + + /** + * Sets whether to catch exceptions or not during commands execution. + * + * @param Boolean $boolean Whether to catch exceptions or not during commands execution + * + * @api + */ + public function setCatchExceptions($boolean) + { + $this->catchExceptions = (Boolean) $boolean; + } + + /** + * Sets whether to automatically exit after a command execution or not. + * + * @param Boolean $boolean Whether to automatically exit after a command execution or not + * + * @api + */ + public function setAutoExit($boolean) + { + $this->autoExit = (Boolean) $boolean; + } + + /** + * Gets the name of the application. + * + * @return string The application name + * + * @api + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the application name. + * + * @param string $name The application name + * + * @api + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Gets the application version. + * + * @return string The application version + * + * @api + */ + public function getVersion() + { + return $this->version; + } + + /** + * Sets the application version. + * + * @param string $version The application version + * + * @api + */ + public function setVersion($version) + { + $this->version = $version; + } + + /** + * Returns the long version of the application. + * + * @return string The long application version + * + * @api + */ + public function getLongVersion() + { + if ('UNKNOWN' !== $this->getName() && 'UNKNOWN' !== $this->getVersion()) { + return sprintf('%s version %s', $this->getName(), $this->getVersion()); + } + + return 'Console Tool'; + } + + /** + * Registers a new command. + * + * @param string $name The command name + * + * @return Command The newly created command + * + * @api + */ + public function register($name) + { + return $this->add(new Command($name)); + } + + /** + * Adds an array of command objects. + * + * @param Command[] $commands An array of commands + * + * @api + */ + public function addCommands(array $commands) + { + foreach ($commands as $command) { + $this->add($command); + } + } + + /** + * Adds a command object. + * + * If a command with the same name already exists, it will be overridden. + * + * @param Command $command A Command object + * + * @return Command The registered command + * + * @api + */ + public function add(Command $command) + { + $command->setApplication($this); + + if (!$command->isEnabled()) { + $command->setApplication(null); + + return; + } + + 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 \InvalidArgumentException When command name given does not exist + * + * @api + */ + public function get($name) + { + if (!isset($this->commands[$name])) { + throw new \InvalidArgumentException(sprintf('The command "%s" does not exist.', $name)); + } + + $command = $this->commands[$name]; + + if ($this->wantHelps) { + $this->wantHelps = false; + + $helpCommand = $this->get('help'); + $helpCommand->setCommand($command); + + return $helpCommand; + } + + return $command; + } + + /** + * Returns true if the command exists, false otherwise. + * + * @param string $name The command name or alias + * + * @return Boolean true if the command exists, false otherwise + * + * @api + */ + public function has($name) + { + return isset($this->commands[$name]); + } + + /** + * Returns an array of all unique namespaces used by currently registered commands. + * + * It does not returns the global namespace which always exists. + * + * @return array An array of namespaces + */ + public function getNamespaces() + { + $namespaces = array(); + foreach ($this->commands as $command) { + $namespaces[] = $this->extractNamespace($command->getName()); + + foreach ($command->getAliases() as $alias) { + $namespaces[] = $this->extractNamespace($alias); + } + } + + return array_values(array_unique(array_filter($namespaces))); + } + + /** + * Finds a registered namespace by a name or an abbreviation. + * + * @param string $namespace A namespace or abbreviation to search for + * + * @return string A registered namespace + * + * @throws \InvalidArgumentException When namespace is incorrect or ambiguous + */ + public function findNamespace($namespace) + { + $allNamespaces = $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, array())) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + $exact = in_array($namespace, $namespaces, true); + if (count($namespaces) > 1 && !$exact) { + throw new \InvalidArgumentException(sprintf('The namespace "%s" is ambiguous (%s).', $namespace, $this->getAbbreviationSuggestions(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 \InvalidArgumentException When command name is incorrect or ambiguous + * + * @api + */ + 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, array())) { + if (1 == count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new \InvalidArgumentException($message); + } + + // 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 \InvalidArgumentException(sprintf('Command "%s" is ambiguous (%s).', $name, $suggestions)); + } + + 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 + * + * @api + */ + public function all($namespace = null) + { + if (null === $namespace) { + return $this->commands; + } + + $commands = array(); + foreach ($this->commands as $name => $command) { + if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) { + $commands[$name] = $command; + } + } + + return $commands; + } + + /** + * Returns an array of possible abbreviations given a set of names. + * + * @param array $names An array of names + * + * @return array An array of abbreviations + */ + public static function getAbbreviations($names) + { + $abbrevs = array(); + foreach ($names as $name) { + for ($len = strlen($name); $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 boolean $raw Whether to return raw command list + * + * @return string A string representing the Application + * + * @deprecated Deprecated since version 2.3, to be removed in 3.0. + */ + public function asText($namespace = null, $raw = false) + { + $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 Boolean $asDom Whether to return a DOM or an XML string + * + * @return string|\DOMDocument An XML string representing the Application + * + * @deprecated Deprecated since version 2.3, to be removed in 3.0. + */ + public function asXml($namespace = null, $asDom = false) + { + $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) + { + $strlen = function ($string) { + if (!function_exists('mb_strlen')) { + return strlen($string); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return strlen($string); + } + + return mb_strlen($string, $encoding); + }; + + do { + $title = sprintf(' [%s] ', get_class($e)); + $len = $strlen($title); + // 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 + $width = $this->getTerminalWidth() ? $this->getTerminalWidth() - 1 : (defined('HHVM_VERSION') ? 1 << 31 : PHP_INT_MAX); + $formatter = $output->getFormatter(); + $lines = array(); + foreach (preg_split('/\r?\n/', $e->getMessage()) as $line) { + foreach (str_split($line, $width - 4) as $line) { + // pre-format lines to get the right string length + $lineLength = $strlen(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 - $strlen($title))))); + foreach ($lines as $line) { + $messages[] = $formatter->format(sprintf(' %s %s', $line[0], str_repeat(' ', $len - $line[1]))); + } + $messages[] = $emptyLine; + $messages[] = ''; + $messages[] = ''; + + $output->writeln($messages, OutputInterface::OUTPUT_RAW); + + if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $output->writeln('Exception trace:'); + + // exception related properties + $trace = $e->getTrace(); + array_unshift($trace, array( + 'function' => '', + 'file' => $e->getFile() != null ? $e->getFile() : 'n/a', + 'line' => $e->getLine() != null ? $e->getLine() : 'n/a', + 'args' => array(), + )); + + for ($i = 0, $count = count($trace); $i < $count; $i++) { + $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; + $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; + $function = $trace[$i]['function']; + $file = isset($trace[$i]['file']) ? $trace[$i]['file'] : 'n/a'; + $line = isset($trace[$i]['line']) ? $trace[$i]['line'] : 'n/a'; + + $output->writeln(sprintf(' %s%s%s() at %s:%s', $class, $type, $function, $file, $line)); + } + + $output->writeln(""); + $output->writeln(""); + } + } while ($e = $e->getPrevious()); + + if (null !== $this->runningCommand) { + $output->writeln(sprintf('%s', sprintf($this->runningCommand->getSynopsis(), $this->getName()))); + $output->writeln(""); + $output->writeln(""); + } + } + + /** + * Tries to figure out the terminal width in which this application runs + * + * @return int|null + */ + protected function getTerminalWidth() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[0]; + } + + /** + * Tries to figure out the terminal height in which this application runs + * + * @return int|null + */ + protected function getTerminalHeight() + { + $dimensions = $this->getTerminalDimensions(); + + return $dimensions[1]; + } + + /** + * Tries to figure out the terminal dimensions based on the current environment + * + * @return array Array containing width and height + */ + public function getTerminalDimensions() + { + if ($this->terminalDimensions) { + return $this->terminalDimensions; + } + + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + // extract [w, H] from "wxh (WxH)" + if (preg_match('/^(\d+)x\d+ \(\d+x(\d+)\)$/', trim(getenv('ANSICON')), $matches)) { + return array((int) $matches[1], (int) $matches[2]); + } + // extract [w, h] from "wxh" + if (preg_match('/^(\d+)x(\d+)$/', $this->getConsoleMode(), $matches)) { + return array((int) $matches[1], (int) $matches[2]); + } + } + + if ($sttyString = $this->getSttyColumns()) { + // extract [w, h] from "rows h; columns w;" + if (preg_match('/rows.(\d+);.columns.(\d+);/i', $sttyString, $matches)) { + return array((int) $matches[2], (int) $matches[1]); + } + // extract [w, h] from "; h rows; w columns" + if (preg_match('/;.(\d+).rows;.(\d+).columns/i', $sttyString, $matches)) { + return array((int) $matches[2], (int) $matches[1]); + } + } + + return array(null, null); + } + + /** + * Sets terminal dimensions. + * + * Can be useful to force terminal dimensions for functional tests. + * + * @param integer $width The width + * @param integer $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('dialog')) { + $inputStream = $this->getHelperSet()->get('dialog')->getInputStream(); + if (!@posix_isatty($inputStream)) { + $input->setInteractive(false); + } + } + + if (true === $input->hasParameterOption(array('--quiet', '-q'))) { + $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); + } 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 integer 0 if everything went fine, or an error code + */ + 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) { + return $command->run($input, $output); + } + + $event = new ConsoleCommandEvent($command, $input, $output); + $this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event); + + try { + $exitCode = $command->run($input, $output); + } catch (\Exception $e) { + $event = new ConsoleTerminateEvent($command, $input, $output, $e->getCode()); + $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event); + + $event = new ConsoleExceptionEvent($command, $input, $output, $e, $event->getExitCode()); + $this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event); + + throw $event->getException(); + } + + $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(), + new ProgressHelper(), + new TableHelper(), + )); + } + + /** + * Runs and parses stty -a if it's available, suppressing any error output + * + * @return string + */ + private function getSttyColumns() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')); + $process = proc_open('stty -a | grep columns', $descriptorspec, $pipes, null, null, array('suppress_errors' => true)); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + return $info; + } + } + + /** + * Runs and parses mode CON if it's available, suppressing any error output + * + * @return string x or null if it could not be parsed + */ + private function getConsoleMode() + { + if (!function_exists('proc_open')) { + return; + } + + $descriptorspec = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')); + $process = proc_open('mode CON', $descriptorspec, $pipes, null, null, array('suppress_errors' => true)); + if (is_resource($process)) { + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + if (preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { + return $matches[2].'x'.$matches[1]; + } + } + } + + /** + * 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 array 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 || 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); + } +} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Input\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) + * + * @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) + { + $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(<<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 Boolean|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 = (!$line && strlen($line) == 0) ? false : rtrim($line); + } + + return $line; + } + + public function getProcessIsolation() + { + return $this->processIsolation; + } + + public function setProcessIsolation($processIsolation) + { + $this->processIsolation = (Boolean) $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.'); + } + } +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package phpdcd + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @since File available since Release 1.0.0 + */ + +namespace SebastianBergmann\PHPDCD\Log; + +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @version Release: @package_version@ + * @link http://github.com/sebastianbergmann/phpdcd/tree + * @since Class available since Release 1.0.0 + */ +class Text +{ + /** + * Prints a result set from PHPDCD_Detector::detectDeadCode(). + * + * @param Symfony\Component\Console\Output\OutputInterface $output + * @param array $result + */ + public function printResult(OutputInterface $output, array $result) + { + foreach ($result as $name => $source) { + $output->writeln( + sprintf( + " - %s()\n LOC: %d, declared in %s:%d\n", + $name, + $source['loc'], + $source['file'], + $source['line'] + ) + ); + } + } +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package phpdcd + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @since File available since Release 1.0.0 + */ + +namespace SebastianBergmann\PHPDCD; + +/** + * PHPDCD code analyser to be used on a body of source code. + * + * Analyses given source code (files) for declared and called functions + * and aggregates this information. + * + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @version Release: @package_version@ + * @link http://github.com/sebastianbergmann/phpdcd/tree + * @since Class available since Release 1.0.0 + */ +class Analyser +{ + /** + * Function declaration mapping: maps declared function name to file and line number + * TODO: make mapping to file and line number optional for memory usage reduction? + * @var array + */ + private $functionDeclarations = array(); + + /** + * Function call mapping: maps "callees" to array of "callers" + * TODO: make callers array optional for memory usage reduction? + * @var array + */ + private $functionCalls = array(); + + /** + * Class hierarchy data: maps classes to their direct parent. + * @var array + */ + private $classParents = array(); + + + public function getFunctionDeclarations() + { + return $this->functionDeclarations; + } + + /** + * Get function calls we detected + * @return array maps "callees" to array of "callers" + */ + public function getFunctionCalls() + { + // Resolve parent(class) calls if possible + foreach ($this->functionCalls as $call => $callers) { + if (strpos($call, 'parent(') === 0) { + preg_match('/parent\\((.*?)\\)::(.*)/', $call, $matches); + $class = $matches[1]; + $method = $matches[2]; + foreach ($this->getAncestors($class) as $ancestor) { + $resolvedCall = $ancestor . '::' . $method; + if (isset($this->functionDeclarations[$resolvedCall])) { + $this->functionCalls[$resolvedCall] = $callers; + // TODO: also remove unresolved parent(class) entries? + break; + } + } + } + } + + return $this->functionCalls; + } + + /** + * Get array of a class's ancestors. + * @param $child + * @return array of ancestors + */ + public function getAncestors($child) + { + $ancestors = array(); + while (isset($this->classParents[$child])) { + $child = $this->classParents[$child]; + if (in_array($child, $ancestors)) { + $cycle = implode(' -> ', $ancestors) . ' -> ' . $child; + throw new \RuntimeException('Class hierarchy cycle detected: ' . $cycle); + } + $ancestors[] = $child; + } + return $ancestors; + } + + /** + * Build a mapping between parent classes and all their descendants + * @return array maps each parent classes to array of its subclasses, subsubclasses, ... + */ + public function getClassDescendants() + { + $descendants = array(); + foreach ($this->classParents as $child => $parent) { + // Direct child + $descendants[$parent][] = $child; + // Store child for further ancestors + $ancestor = $parent; + while (isset($this->classParents[$ancestor])) { + $ancestor = $this->classParents[$ancestor]; + $descendants[$ancestor][] = $child; + } + } + return $descendants; + } + + /** + * Analyse a PHP source code file for defined and called functions. + * @param $filename + */ + public function analyseFile($filename) + { + $sourceCode = file_get_contents($filename); + return $this->analyseSourceCode($sourceCode, $filename); + } + + /** + * Analyse PHP source code for defined and called functions + * + * @param string $sourceCode source code. + * @param string $filename optional file name to use in declaration definition + */ + public function analyseSourceCode($sourceCode, $filename = 'undefined') + { + + $blocks = array(); + $currentBlock = null; + $currentClass = ''; + $currentFunction = ''; + $currentInterface = ''; + $namespace = ''; + $variables = array(); + + $tokens = new \PHP_Token_Stream($sourceCode); + $count = count($tokens); + + for ($i = 0; $i < $count; $i++) { + if ($tokens[$i] instanceof \PHP_Token_NAMESPACE) { + $namespace = $tokens[$i]->getName(); + } elseif ($tokens[$i] instanceof \PHP_Token_CLASS) { + $currentClass = $tokens[$i]->getName(); + + if ($namespace != '') { + $currentClass = $namespace . '\\' . $currentClass; + } + + $currentBlock = $currentClass; + } elseif ($tokens[$i] instanceof \PHP_Token_EXTENDS + && $tokens[$i+2] instanceof \PHP_Token_STRING) { + // Store parent-child class relationship. + $this->classParents[$currentClass] = (string)$tokens[$i+2]; + } elseif ($tokens[$i] instanceof \PHP_Token_INTERFACE) { + $currentInterface = $tokens[$i]->getName(); + + if ($namespace != '') { + $currentInterface = $namespace . '\\' . $currentClass; + } + + $currentBlock = $currentInterface; + } elseif ($tokens[$i] instanceof \PHP_Token_NEW && + !$tokens[$i+2] instanceof \PHP_Token_VARIABLE) { + if ($tokens[$i-1] instanceof \PHP_Token_EQUAL) { + $j = -1; + } elseif ($tokens[$i-1] instanceof \PHP_Token_WHITESPACE && + $tokens[$i-2] instanceof \PHP_Token_EQUAL) { + $j = -2; + } else { + continue; + } + + if ($tokens[$i+$j-1] instanceof \PHP_Token_WHITESPACE) { + $j--; + } + + if ($tokens[$i+$j-1] instanceof \PHP_Token_VARIABLE) { + $name = (string)$tokens[$i+$j-1]; + $variables[$name] = (string)$tokens[$i+2]; + } elseif ($tokens[$i+$j-1] instanceof \PHP_Token_STRING && + $tokens[$i+$j-2] instanceof \PHP_Token_OBJECT_OPERATOR && + $tokens[$i+$j-3] instanceof \PHP_Token_VARIABLE) { + $name = (string)$tokens[$i+$j-3] . '->' . + (string)$tokens[$i+$j-1]; + $variables[$name] = (string)$tokens[$i+2]; + } + } elseif ($tokens[$i] instanceof \PHP_Token_FUNCTION) { + if ($currentInterface != '') { + continue; + } + + // Ignore abstract methods. + for ($j=1; $j<=4; $j++) { + if (isset($tokens[$i-$j]) && + $tokens[$i-$j] instanceof \PHP_Token_ABSTRACT) { + continue 2; + } + } + + $function = $tokens[$i]->getName(); + + if ($function == 'anonymous function') { + continue; + } + + $variables = $tokens[$i]->getArguments(); + + if ($currentClass != '') { + $function = $currentClass . '::' . $function; + $variables['$this'] = $currentClass; + } + + $currentFunction = $function; + $currentBlock = $currentFunction; + + $this->functionDeclarations[$function] = array( + 'file' => $filename, 'line' => $tokens[$i]->getLine() + ); + } elseif ($tokens[$i] instanceof \PHP_Token_OPEN_CURLY + || $tokens[$i] instanceof \PHP_Token_CURLY_OPEN + || $tokens[$i] instanceof \PHP_Token_DOLLAR_OPEN_CURLY_BRACES ) { + array_push($blocks, $currentBlock); + $currentBlock = null; + } elseif ($tokens[$i] instanceof \PHP_Token_CLOSE_CURLY) { + $block = array_pop($blocks); + + if ($block == $currentClass) { + $currentClass = ''; + } elseif ($block == $currentFunction) { + $this->functionDeclarations[$currentFunction]['loc'] = + $tokens[$i]->getLine() - $this->functionDeclarations[$currentFunction]['line'] + 1; + $currentFunction = ''; + $variables = array(); + } + } elseif ($tokens[$i] instanceof \PHP_Token_OPEN_BRACKET) { + for ($j = 1; $j <= 4; $j++) { + if (isset($tokens[$i-$j]) && + $tokens[$i-$j] instanceof \PHP_Token_FUNCTION) { + continue 2; + } + } + + if ($tokens[$i-1] instanceof \PHP_Token_STRING) { + $j = -1; + } elseif ($tokens[$i-1] instanceof \PHP_Token_WHITESPACE && + $tokens[$i-2] instanceof \PHP_Token_STRING) { + $j = -2; + } else { + continue; + } + + $function = (string)$tokens[$i+$j]; + $lookForNamespace = true; + + if (isset($tokens[$i+$j-2]) && + $tokens[$i+$j-2] instanceof \PHP_Token_NEW) { + $function .= '::__construct'; + } elseif ((isset($tokens[$i+$j-1]) && + $tokens[$i+$j-1] instanceof \PHP_Token_OBJECT_OPERATOR) || + (isset($tokens[$i+$j-2]) && + $tokens[$i+$j-2] instanceof \PHP_Token_OBJECT_OPERATOR)) { + $_function = $tokens[$i+$j]; + $lookForNamespace = false; + + if ($tokens[$i+$j-1] instanceof \PHP_Token_OBJECT_OPERATOR) { + $j -= 2; + } else { + $j -= 3; + } + + if ($tokens[$i+$j] instanceof \PHP_Token_VARIABLE) { + if (isset($variables[(string)$tokens[$i+$j]])) { + $function = $variables[(string)$tokens[$i+$j]] . + '::' . $_function; + } else { + $function = '::' . $_function; + } + } elseif ($tokens[$i+$j] instanceof \PHP_Token_STRING && + $tokens[$i+$j-1] instanceof \PHP_Token_OBJECT_OPERATOR && + $tokens[$i+$j-2] instanceof \PHP_Token_VARIABLE) { + $variable = (string)$tokens[$i+$j-2] . '->' . + (string)$tokens[$i+$j]; + + if (isset($variables[$variable])) { + $function = $variables[$variable] . '::' . + $_function; + } + } + } elseif ($tokens[$i+$j-1] instanceof \PHP_Token_DOUBLE_COLON) { + $class = (string)$tokens[$i+$j-2]; + + if ($class == 'self' || $class == 'static') { + $class = $currentClass; + } elseif ($class == 'parent') { + $class = "parent($currentClass)"; + } + + $function = $class . '::' . $function; + $j -= 2; + } + + if ($lookForNamespace) { + while ($tokens[$i+$j-1] instanceof \PHP_Token_NS_SEPARATOR) { + $function = $tokens[$i+$j-2] . '\\' . $function; + $j -= 2; + } + } + + if (!isset($this->functionCalls[$function])) { + $this->functionCalls[$function] = array(); + } + $this->functionCalls[$function][] = $currentFunction; + } + } + } +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package phpdcd + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @since File available since Release 1.0.0 + */ + +namespace SebastianBergmann\PHPDCD; + +/** + * PHPDCD detector for unused functions. + * + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @version Release: @package_version@ + * @link http://github.com/sebastianbergmann/phpdcd/tree + * @since Class available since Release 1.0.0 + */ +class Detector +{ + /** + * @param array $files + * @param boolean $recursive + * @return array + */ + public function detectDeadCode(array $files, $recursive = false) + { + + // Analyse files and collect declared and called functions + $analyser = new Analyser(); + foreach ($files as $file) { + $analyser->analyseFile($file); + } + + // Get info on declared and called functions. + $declared = $analyser->getFunctionDeclarations(); + $called = $analyser->getFunctionCalls(); + $classDescendants = $analyser->getClassDescendants(); + + // Search for declared, unused functions. + $result = array(); + foreach ($declared as $name => $source) { + if (!isset($called[$name])) { + // Unused function/method at first sight. + $used = false; + // For methods: check calls from subclass instances as well + $parts = explode('::', $name); + if (count($parts) == 2) { + $class = $parts[0]; + $subclasses = isset($classDescendants[$class]) ? $classDescendants[$class] : array(); + foreach ($subclasses as $subclass) { + if (isset($called[$subclass . '::' . $parts[1]])) { + $used = true; + break; + } + } + } + + if (!$used) { + $result[$name] = $source; + } + } + } + + if ($recursive) { + $done = false; + + while (!$done) { + $done = true; + + foreach ($called as $callee => $callers) { + $_called = false; + + foreach ($callers as $caller) { + if (!isset($result[$caller])) { + $_called = true; + break; + } + } + + if (!$_called) { + if (isset($declared[$callee])) { + $result[$callee] = $declared[$callee]; + } + + $done = false; + + unset($called[$callee]); + } + } + } + } + + ksort($result); + + return $result; + } +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package phpdcd + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @since File available since Release 1.0.0 + */ + +namespace SebastianBergmann\PHPDCD\CLI; + +use SebastianBergmann\PHPDCD\Detector; +use SebastianBergmann\PHPDCD\Log\Text; +use SebastianBergmann\FinderFacade\FinderFacade; +use Symfony\Component\Console\Command\Command as AbstractCommand; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @link http://github.com/sebastianbergmann/phpdcd/tree + * @since Class available since Release 1.0.0 + */ +class Command extends AbstractCommand +{ + /** + * Configures the current command. + */ + protected function configure() + { + $this->setName('phpdcd') + ->setDefinition( + array( + new InputArgument( + 'values', + InputArgument::IS_ARRAY + ) + ) + ) + ->addOption( + 'names', + null, + InputOption::VALUE_REQUIRED, + 'A comma-separated list of file names to check', + array('*.php') + ) + ->addOption( + 'names-exclude', + null, + InputOption::VALUE_REQUIRED, + 'A comma-separated list of file names to exclude', + array() + ) + ->addOption( + 'exclude', + null, + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Exclude a directory from code analysis' + ) + ->addOption( + 'recursive', + null, + InputOption::VALUE_NONE, + 'Report code as dead if it is only called by dead code' + ); + } + + /** + * Executes the current command. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + * + * @return null|integer null or 0 if everything went fine, or an error code + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $finder = new FinderFacade( + $input->getArgument('values'), + $input->getOption('exclude'), + $this->handleCSVOption($input, 'names'), + $this->handleCSVOption($input, 'names-exclude') + ); + + $files = $finder->findFiles(); + + if (empty($files)) { + $output->writeln('No files found to scan'); + exit(1); + } + + $quiet = $output->getVerbosity() == OutputInterface::VERBOSITY_QUIET; + + $detector = new Detector; + + $result = $detector->detectDeadCode( + $files, + $input->getOption('recursive') + ); + + if (!$quiet) { + $printer = new Text; + $printer->printResult($output, $result); + + $output->writeln(\PHP_Timer::resourceUsage()); + } + } + + /** + * @param Symfony\Component\Console\Input\InputOption $input + * @param string $option + * @return array + */ + private function handleCSVOption(InputInterface $input, $option) + { + $result = $input->getOption($option); + + if (!is_array($result)) { + $result = explode(',', $result); + array_map('trim', $result); + } + + return $result; + } +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package phpdcd + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @since File available since Release 1.0.0 + */ + +namespace SebastianBergmann\PHPDCD\CLI; + +use SebastianBergmann\Version; +use Symfony\Component\Console\Application as AbstractApplication; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Input\ArrayInput; + +/** + * TextUI frontend for PHPDCD. + * + * @author Sebastian Bergmann + * @copyright 2009-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @link http://github.com/sebastianbergmann/phpdcd/tree + * @since Class available since Release 1.0.0 + */ +class Application extends AbstractApplication +{ + public function __construct() + { + $version = new Version('1.0.2', dirname(dirname(__DIR__))); + parent::__construct('phpdcd', $version->getVersion()); + } + + /** + * 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 'phpdcd'; + } + + /** + * Gets the default commands that should always be available. + * + * @return array An array of default Command instances + */ + protected function getDefaultCommands() + { + $defaultCommands = parent::getDefaultCommands(); + + $defaultCommands[] = new Command; + + return $defaultCommands; + } + + /** + * Overridden so that the application doesn't expect the command + * name to be the first argument. + */ + public function getDefinition() + { + $inputDefinition = parent::getDefinition(); + $inputDefinition->setArguments(); + + return $inputDefinition; + } + + /** + * Runs the current application. + * + * @param InputInterface $input An Input instance + * @param OutputInterface $output An Output instance + * + * @return integer 0 if everything went fine, or an error code + */ + public function doRun(InputInterface $input, OutputInterface $output) + { + if (!$input->hasParameterOption('--quiet')) { + $output->write( + sprintf( + "phpdcd %s by Sebastian Bergmann.\n\n", + $this->getVersion() + ) + ); + } + + if ($input->hasParameterOption('--version') || + $input->hasParameterOption('-V')) { + exit; + } + + if (!$input->getFirstArgument()) { + $input = new ArrayInput(array('--help')); + } + + parent::doRun($input, $output); + } +} +sebastian/phpdcd: 1.0.2 +phpunit/php-timer: 1.0.5 +phpunit/php-token-stream: 1.2.2 +sebastian/finder-facade: 1.1.0 +sebastian/version: 1.0.3 +symfony/console: v2.4.3 +symfony/finder: v2.4.3 +theseer/fdomdocument: 1.5.0 +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package Version + * @author Sebastian Bergmann + * @copyright 2013-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @link http://github.com/sebastianbergmann/version + * @since File available since Release 1.0.0 + */ + +namespace SebastianBergmann; + +/** + * @package Version + * @author Sebastian Bergmann + * @copyright 2013-2014 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @link http://github.com/sebastianbergmann/version + * @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 . '-' . $git[2]; + } + } + } + + return $this->version; + } + + /** + * @param string $path + * @return boolean|string + */ + private function getGitInformation($path) + { + if (!is_dir($path . DIRECTORY_SEPARATOR . '.git')) { + return false; + } + + $dir = getcwd(); + chdir($path); + $result = @exec('git describe --tags 2>&1', $output, $returnCode); + chdir($dir); + + if ($returnCode !== 0) { + return false; + } + + return $result; + } +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package FinderFacade + * @author Sebastian Bergmann + * @copyright 2012-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @since File available since Release 1.0.0 + */ + +namespace SebastianBergmann\FinderFacade +{ + use Symfony\Component\Finder\Finder; + + /** + * Convenience wrapper for Symfony's Finder component. + * + * @author Sebastian Bergmann + * @copyright 2012-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @version Release: @package_version@ + * @link http://github.com/sebastianbergmann/finder-facade/tree + * @since Class available since Release 1.0.0 + */ + class FinderFacade + { + /** + * @var array + */ + protected $items = array(); + + /** + * @var array + */ + protected $excludes = array(); + + /** + * @var array + */ + protected $names = array(); + + + /** + * @var array + */ + protected $notNames = array(); + + /** + * @param array $items + * @param array $excludes + * @param array $names + * @param array $notNames + */ + public function __construct(array $items = array(), array $excludes = array(), array $names = array(), array $notNames = array()) + { + $this->items = $items; + $this->excludes = $excludes; + $this->names = $names; + $this->notNames = $notNames; + } + + /** + * @return array + */ + public function findFiles() + { + $files = array(); + $finder = new Finder; + $iterate = FALSE; + + foreach ($this->items as $item) { + if (!is_file($item)) { + $finder->in($item); + $iterate = TRUE; + } + + else { + $files[] = realpath($item); + } + } + + foreach ($this->excludes as $exclude) { + $finder->exclude($exclude); + } + + foreach ($this->names as $name) { + $finder->name($name); + } + + foreach ($this->notNames as $notName) { + $finder->notName($notName); + } + + if ($iterate) { + foreach ($finder as $file) { + $files[] = $file->getRealpath(); + } + } + + return $files; + } + + /** + * @param string $file + */ + public function loadConfiguration($file) + { + $configuration = new Configuration($file); + $configuration = $configuration->parse(); + + $this->items = $configuration['items']; + $this->excludes = $configuration['excludes']; + $this->names = $configuration['names']; + $this->notNames = $configuration['notNames']; + } + } +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package FinderFacade + * @author Sebastian Bergmann + * @copyright 2012-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @since File available since Release 1.0.0 + */ + +namespace SebastianBergmann\FinderFacade +{ + use TheSeer\fDOM\fDOMDocument; + + /** + * + * + * + * /path/to/directory + * /path/to/file + * + * /path/to/directory + * *.php + * + * + * + * @author Sebastian Bergmann + * @copyright 2012-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @version Release: @package_version@ + * @link http://github.com/sebastianbergmann/finder-facade/tree + * @since Class available since Release 1.0.0 + */ + class Configuration + { + /** + * @var string + */ + protected $basePath; + + /** + * @var fDOMDocument + */ + protected $xml; + + /** + * @param string $file + */ + public function __construct($file) + { + $this->basePath = dirname($file); + + $this->xml = new fDOMDocument; + $this->xml->load($file); + } + + /** + * @param string $xpath + * @return array + */ + public function parse($xpath = '') + { + $result = array( + 'items' => array(), 'excludes' => array(), 'names' => array(), 'notNames' => array() + ); + + foreach ($this->xml->getDOMXPath()->query($xpath . 'include/directory') as $item) { + $result['items'][] = $this->toAbsolutePath($item->nodeValue); + } + + foreach ($this->xml->getDOMXPath()->query($xpath . 'include/file') as $item) { + $result['items'][] = $this->toAbsolutePath($item->nodeValue); + } + + foreach ($this->xml->getDOMXPath()->query($xpath . 'exclude') as $exclude) { + $result['excludes'][] = $exclude->nodeValue; + } + + foreach ($this->xml->getDOMXPath()->query($xpath . 'name') as $name) { + $result['names'][] = $name->nodeValue; + } + + foreach ($this->xml->getDOMXPath()->query($xpath . 'notName') as $name) { + $result['notNames'][] = $name->nodeValue; + } + + return $result; + } + + /** + * @param string $path + * @return string + */ + protected function toAbsolutePath($path) + { + // Check whether the path is already absolute. + if ($path[0] === '/' || $path[0] === '\\' || + (strlen($path) > 3 && ctype_alpha($path[0]) && + $path[1] === ':' && ($path[2] === '\\' || $path[2] === '/'))) { + return $path; + } + + // Check whether a stream is used. + if (strpos($path, '://') !== FALSE) { + return $path; + } + + return $this->basePath . DIRECTORY_SEPARATOR . $path; + } + } +} + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Arne Blankerts nor the names of contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @copyright Arne Blankerts , All rights reserved. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://github.com/theseer/fdomdocument + * + */ + +namespace TheSeer\fDOM { + + /** + * fDomElement + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @access public + * + */ + class fDOMElement extends \DOMElement { + + public function __toString() { + return $this->C14N(); + } + + /** + * Forward to fDomDocument->query() + * + * @param string $q XPath to use + * @param \DOMNode $ctx \DOMNode to overwrite context + * @param boolean $registerNodeNS Register flag pass thru + * + * @return \DomNodeList + */ + public function query($q, \DOMNode $ctx = null, $registerNodeNS = true) { + return $this->ownerDocument->query($q, $ctx ? $ctx : $this, $registerNodeNS); + } + + /** + * Forward to fDomDocument->queryOne() + * + * @param string $q XPath to use + * @param \DOMNode $ctx (optional) \DOMNode to overwrite context + * @param boolean $registerNodeNS Register flag pass thru + * + * @return mixed + */ + public function queryOne($q, \DOMNode $ctx = null, $registerNodeNS = true) { + return $this->ownerDocument->queryOne($q, $ctx ? $ctx : $this, $registerNodeNS); + } + + /** + * Forward to fDomDocument->select() + * + * @param string $selector A CSS Level 3 Selector string + * @param \DOMNode $ctx + * @param bool $registerNodeNS + * + * @return \DOMNodeList + */ + + public function select($selector, \DOMNode $ctx = null, $registerNodeNS = true) { + return $this->ownerDocument->select($selector, $ctx, $registerNodeNS); + } + + /** + * Parse and append XML String to node + * + * @param String $str string to process + * + * @return fDomDocumentFragment Reference to the created Fragment + */ + public function appendXML($str) { + $frag = $this->ownerDocument->createDocumentFragment(); + $frag->appendXML($str); + $this->appendChild($frag); + return $frag; + } + + /** + * Create a new element and append it + * + * @param string $name Name of not element to create + * @param string $content Optional content to be set + * + * @return fDOMElement Reference to created fDOMElement + */ + public function appendElement($name, $content = null) { + $node = $this->ownerDocument->createElement($name, $content); + $this->appendChild($node); + return $node; + } + + /** + * Create a new element in given namespace and append it + * + * @param string $ns Namespace of node to create + * @param string $name Name of not element to create + * @param string $content Optional content to be set + * + * @return fDOMElement Reference to created fDOMElement + */ + public function appendElementNS($ns, $name, $content = null) { + $node = $this->ownerDocument->createElementNS($ns, $name, $content); + $this->appendChild($node); + return $node; + } + + /** + * Create a new element in given namespace and append it + * + * @param string $prefix Namespace prefix for node to create + * @param string $name Name of not element to create + * @param string $content Optional content to be set + * + * @return fDOMElement Reference to created fDOMElement + */ + public function appendElementPrefix($prefix, $name, $content = null) { + $node = $this->ownerDocument->createElementPrefix($prefix, $name, $content); + $this->appendChild($node); + return $node; + } + + /** + * Create a new text node and append it + * + * @param string $content Text content to be added + * + * @return \DOMText + */ + public function appendTextNode($content) { + $text = $this->ownerDocument->createTextNode($content); + $this->appendChild($text); + return $text; + } + + /** + * Wrapper to DomElement->getAttribute with default value option + * + * Note: A set but emptry attribute does NOT trigger use of the default + * + * @param string $attr Attribute to access + * @param string $default Default value to use if the attribute is not set + * + * @return string + */ + public function getAttribute($attr, $default='') { + return $this->hasAttribute($attr) ? parent::getAttribute($attr) : $default; + } + + /** + * Wrapper to DomElement->getAttributeNS with default value option + * + * Note: A set but empty attribute does NOT trigger use of the default + * + * @param string $ns Namespace of attribute + * @param string $attr Attribute to access + * @param string $default Default value to use if the attribute is not set + * + * @return string + */ + public function getAttributeNS($ns, $attr, $default='') { + return $this->hasAttributeNS($ns, $attr) ? parent::getAttributeNS($ns, $attr) : $default; + } + + /** + * Wrapper to DOMElement::setAttribute with additional entities support + * + * @param string $attr Attribute name to set + * @param string $value Value to set attribute to + * @param bool $keepEntitites Flag to signale if entities should be kept + * + * @throws fDOMException + * + * @return DOMAttr + * + * @see DOMElement::setAttribute() + */ + public function setAttribute($attr, $value, $keepEntities=false) { + if ($keepEntities === true) { + $attrNode = $this->ownerDocument->createAttribute($attr); + if (!$attrNode) { + throw new fDOMException("Setting attribute '$attr' failed.", fDOMException::SetFailedError); + } + $attrNode->value = $value; + $this->appendChild($attrNode); + return $attrNode; + } + return parent::setAttribute($attr, $value); + } + + /** + * Wrapper to namespace aware DOMElement::setAttributeNS with additional entities support + * + * @param string $ns namespace attribute should be in + * @param string $attr Attribute name to set + * @param string $value Value to set attribute to + * @param boolean $keepEntitites Flag to signale if entities should be kept + * + * @return \DOMAttr + * + * @see DOMElement::setAttribute() + */ + public function setAttributeNS($ns, $attr, $value, $keepEntities=false) { + if ($keepEntities === true) { + $attrNode = $this->ownerDocument->createAttributeNS($ns, $attr); + if (!$attrNode) { + throw new fDOMException("Setting attribute '$attr' failed.", fDOMException::SetFailedError); + } + $attrNode->value = $value; + $this->appendChild($attrNode); + return $attrNode; + } + return parent::setAttributeNS($ns, $attr, $value); + } + + /** + * Helper to add multiple attributes to an element + * + * @param array $attr Attributes to add as key-value pair + * @param bool $keepEntities Flag wether to keep entities + * + * @return array List with references to created DOMAttr + */ + public function setAttributes(array $attr, $keepEntities=false) { + $attList = array(); + foreach($attr as $name => $value) { + $attList[] = $this->setAttribute($name, $value, $keepEntities); + } + return $attList; + } + + /** + * Helper to add multiple attributes with the given namespace and prefix + * + * @param string $ns Namespace of attribute + * @param string $prefix Namespace prefix for attribute to create + * @param array $attr Attributes to add + * @param bool $keepEntities Flag wether to keep entities + * + * @return void + */ + public function setAttributesNS($ns, $prefix, array $attr, $keepEntities=false) { + foreach($attr as $name => $value) { + $this->setAttributeNS($ns, $prefix.':'.$name, $value, $keepEntities); + } + } + + /** + * Helper method to get children by name + * + * @param string $tagName tagname to search for + * + * @return DomNodeList + */ + public function getChildrenByTagName($tagName) { + return $this->query("*[local-name()='$tagName']"); + } + + /** + * Helper method to get children by name and namespace + * + * @param string $ns namespace nodes have to be in + * @param string $tagName tagname to search for + * + * @return DomNodeList + */ + public function getChildrenByTagNameNS($ns, $tagName) { + return $this->query("*[local-name()='$tagName' and namespace-uri()='$ns']"); + } + + /** + * Check if the given node is in the same document + * + * @param \DomNode $node Node to compare with + * + * @return boolean true on match, false if they differ + * + */ + public function inSameDocument(\DomNode $node) { + return $this->ownerDocument->inSameDocument($node); + } + + /** + * Wrapper to DomDocument::saveXML() with current node as context + * + * @return string + */ + public function saveXML() { + return $this->ownerDocument->saveXML($this); + } + + /** + * Wrapper to DomDocument::saveHTML() with current node as context + * + * @return string + */ + public function saveHTML() { + return $this->ownerDocument->saveHTML($this); + } + + } // fDOMElement + +} + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Arne Blankerts nor the names of contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @copyright Arne Blankerts , All rights reserved. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://github.com/theseer/fdomdocument + * + */ + +namespace TheSeer\fDOM { + + /** + * fDomNode + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @access public + * + */ + class fDOMNode extends \DOMNode { + + public function __toString() { + return $this->C14N(); + } + + /** + * Forward to fDomDocument->query() + * + * @param string $q XPath to use + * @param \DOMNode $ctx \DOMNode to overwrite context + * @param boolean $registerNodeNS Register flag pass thru + * + * @return \DomNodeList + */ + public function query($q, \DOMNode $ctx = null, $registerNodeNS = true) { + return $this->ownerDocument->query($q, $ctx ? $ctx : $this, $registerNodeNS); + } + + /** + * Forward to fDomDocument->queryOne() + * + * @param string $q XPath to use + * @param \DOMNode $ctx (optional) \DOMNode to overwrite context + * @param boolean $registerNodeNS Register flag pass thru + * + * @return mixed + */ + public function queryOne($q, \DOMNode $ctx = null, $registerNodeNS = true) { + return $this->ownerDocument->queryOne($q, $ctx ? $ctx : $this, $registerNodeNS); + } + + /** + * Forward to fDomDocument->select() + * + * @param string $selector A CSS Level 3 Selector string + * @param \DOMNode $ctx + * @param bool $registerNodeNS + * + * @return \DOMNodeList + */ + + public function select($selector, \DOMNode $ctx = null, $registerNodeNS = true) { + return $this->ownerDocument->select($selector, $ctx, $registerNodeNS); + } + + /** + * Check if the given node is in the same document + * + * @param \DomNode $node Node to compare with + * + * @return boolean true on match, false if they differ + * + */ + public function inSameDocument(\DOMNode $node) { + return $this->ownerDocument->inSameDocument($node); + } + + /** + * Wrapper to DomDocument::saveXML() with current node as context + * + * @return string + */ + public function saveXML() { + return $this->ownerDocument->saveXML($this); + } + + /** + * Wrapper to DomDocument::saveHTML() with current node as context + * + * @return string + */ + public function saveHTML() { + return $this->ownerDocument->saveHTML($this); + } + + + + } // fDOMNode + +} + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Arne Blankerts nor the names of contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @copyright Arne Blankerts , All rights reserved. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://github.com/theseer/fdomdocument + * + */ + +namespace TheSeer\fDOM { + + /** + * Class XPathQuery + * + * @package TheSeer\fDOM + */ + class XPathQuery { + + /** + * @var string + */ + private $query; + + /** + * Key-value Map for bound values + * + * @var array + */ + private $values = array(); + + /** + * @param string $query + */ + public function __construct($query) { + $this->setQuery($query); + } + + /** + * @param string $query + */ + private function setQuery($query) { + $this->query = $query; + $res = preg_match_all('/(:(\w*))/', $query, $matches); + if ($res > 0) { + $this->values = array_fill_keys($matches[2], ''); + } + } + + public function getKeys() { + return array_keys($this->values); + } + + public function bind($key, $value) { + if (!array_key_exists($key, $this->values)) { + throw new XPathQueryException("'$key' not found in query'", XPathQueryException::KeyNotFound ); + } + $this->values[$key] = $value; + } + + public function generate(\DOMNode $ctx, array $values = NULL) { + return $this->buildQuery($this->getXPathObjectFor($ctx), $values); + } + + public function evaluate(\DOMNode $ctx, array $values = NULL, $registerNodeNS = TRUE) { + $xp = $this->getXPathObjectFor($ctx); + return $xp->evaluate($this->buildQuery($xp, $values), $ctx, $registerNodeNS); + } + + public function query(\DOMNode $ctx, array $values = NULL, $registerNodeNS = TRUE) { + $xp = $this->getXPathObjectFor($ctx); + return $xp->evaluate($this->buildQuery($xp, $values), $ctx, $registerNodeNS); + } + + public function queryOne(\DOMNode $ctx, array $values = NULL, $registerNodeNS = TRUE) { + $xp = $this->getXPathObjectFor($ctx); + return $xp->queryOne($this->buildQuery($xp, $values), $ctx, $registerNodeNS); + } + + private function getXPathObjectFor(\DOMNode $ctx) { + $dom = $ctx instanceof \DOMDocument ? $ctx : $ctx->ownerDocument; + if ($dom instanceOf fDOMDocument) { + return $dom->getDOMXPath(); + } + return new fDOMXPath($dom); + } + + private function buildQuery(fDOMXPath $xp, array $values = NULL) { + $backup = $this->values; + if (count($values) > 0) { + foreach($values as $k => $v) { + $this->bind($k, $v); + } + } + $query = $xp->prepare($this->query, $this->values); + $this->values = $backup; + return $query; + } + } + +} +translator = $translator; + } + + /** + * @param $selector + * + * @return string + */ + public function apply($selector) { + return preg_replace_callback( + '/([a-zA-Z0-9\_\-\*]+):not\(([^\)]*)\)/', + array($this, 'callback'), + $selector + ); + } + + private function callback(array $matches) { + $subresult = preg_replace( + '/^[^\[]+\[([^\]]*)\].*$/', + '$1', + $this->translator->translate($matches[2]) + ); + return $matches[1] . '[not(' . $subresult . ')]'; + } + + } + +} + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Arne Blankerts nor the names of contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @copyright Arne Blankerts , All rights reserved. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://github.com/theseer/fdomdocument + * + */ + +namespace TheSeer\fDOM\CSS { + + /** + * Class Translator + * + * The regular expressions used in this class are heavily inspired by and mostly adopted from + * the css2xpath.js code by Andrea Giammarchi (http://code.google.com/p/css2xpath/). + * The JavaScript version (css2xpath.js) is licensed under the MIT License + * + */ + class Translator { + + /** + * @var array + */ + private $rules; + + /** + * @param string $selector A CSS Selector string + * + * @return string + */ + public function translate($selector) { + foreach($this->getRules() as $rule) { + /** @var RuleInterface $rule */ + $selector = $rule->apply($selector); + } + return '//' . $selector; + } + + /** + * @return array + */ + private function getRules() { + if ($this->rules != NULL) { + return $this->rules; + } + + $this->rules = array( + + // prefix|name + new RegexRule('/([a-zA-Z0-9\_\-\*]+)\|([a-zA-Z0-9\_\-\*]+)/', '$1:$2'), + + // add @ for attribs + new RegexRule("/\[([^\]~\$\*\^\|\!]+)(=[^\]]+)?\]/", '[@$1$2]'), + + // multiple queries + new RegexRule("/\s*,\s*/", '|'), + + // , + ~ > + new RegexRule("/\s*(\+|~|>)\s*/", '$1'), + + //* ~ + > + new RegexRule("/([a-zA-Z0-9\_\-\*])~([a-zA-Z0-9\_\-\*])/", '$1/following-sibling::$2'), + new RegexRule("/([a-zA-Z0-9\_\-\*])\+([a-zA-Z0-9\_\-\*])/", '$1/following-sibling::*[1]/self::$2'), + new RegexRule("/([a-zA-Z0-9\_\-\*])>([a-zA-Z0-9\_\-\*])/", '$1/$2'), + + // all unescaped stuff escaped + new RegexRule("/\[([^=]+)=([^'|'][^\]]*)\]/", '[$1="$2"]'), + + // all descendant or self to // + new RegexRule("/(^|[^a-zA-Z0-9\_\-\*])(#|\.)([a-zA-Z0-9\_\-]+)/", '$1*$2$3'), + new RegexRule("/([\>\+\|\~\,\s])([a-zA-Z\*]+)/", '$1//$2'), + new RegexRule("/\s+\/\//", '//'), + + // :first-child + new RegexRule("/([a-zA-Z0-9\_\-\*]+):first-child/", '*[1]/self::$1'), + + // :last-child + new RegexRule("/([a-zA-Z0-9\_\-\*]+):last-child/", '$1[not(following-sibling::*)]'), + + // :only-child + new RegexRule("/([a-zA-Z0-9\_\-\*]+):only-child/", '*[last()=1]/self::$1'), + + // :empty + new RegexRule("/([a-zA-Z0-9\_\-\*]+):empty/", '$1[not(*) and not(normalize-space())]'), + + // :not + new NotRule($this), + + // :nth-child + new NthChildRule(), + + // :contains(selectors) + new RegexRule('/:contains\(([^\)]*)\)/', '[contains(string(.),"$1")]'), + + // |= attrib + new RegexRule("/\[([a-zA-Z0-9\_\-]+)\|=([^\]]+)\]/", '[@$1=$2 or starts-with(@$1,concat($2,"-"))]'), + + // *= attrib + new RegexRule("/\[([a-zA-Z0-9\_\-]+)\*=([^\]]+)\]/", '[contains(@$1,$2)]'), + + // ~= attrib + new RegexRule("/\[([a-zA-Z0-9\_\-]+)~=([^\]]+)\]/", '[contains(concat(" ",normalize-space(@$1)," "),concat(" ",$2," "))]'), + + // ^= attrib + new RegexRule("/\[([a-zA-Z0-9\_\-]+)\^=([^\]]+)\]/", '[starts-with(@$1,$2)]'), + + // $= attrib + new DollarEqualRule(), + + // != attrib + new RegexRule("/\[([a-zA-Z0-9\_\-]+)\!=([^\]]+)\]/", '[not(@$1) or @$1!=$2]'), + + // ids and classes + new RegexRule("/#([a-zA-Z0-9\_\-]+)/", '[@id="$1"]'), + new RegexRule("/\.([a-zA-Z0-9\_\-]+)/", '[contains(concat(" ",normalize-space(@class)," ")," $1 ")]'), + + // normalize multiple filters + new RegexRule("/\]\[([^\]]+)/", ' and ($1)') + + + ); + return $this->rules; + } + } + +} +=0]/self::' . $matches[1]; + } + case 'odd': { + return $matches[1] . '[(count(preceding-sibling::*) + 1) mod 2=1]'; + } + default: { + $b = !isset($matches[2]) || empty($matches[2]) ? '0' : $matches[2]; + $b = preg_replace('/^([0-9]*)n.*?([0-9]*)$/', '$1+$2', $b); + $b = explode('+', $b); + if (!isset($b[1])) { + $b[1] = '0'; + } + return '*[(position()-' . $b[1] . ') mod ' . $b[0] . '=0 and position()>=' . $b[1] . ']/self::' . $matches[1]; + } + } + } + + } + +} +regex = $regex; + $this->replacement = $replacement; + } + + /** + * @param $selector + * + * @return string + */ + public function apply($selector) { + return preg_replace($this->regex, $this->replacement, $selector); + } + + } + +} + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Arne Blankerts nor the names of contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @copyright Arne Blankerts , All rights reserved. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://github.com/theseer/fdomdocument + * + */ + +namespace TheSeer\fDOM { + + /** + * fDOMXPath extension to PHP's DOMXPath. + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @access public + * + */ + class fDOMXPath extends \DOMXPath { + + protected $doc; + + public function __construct(\DOMDocument $doc) { + parent::__construct($doc); + $this->doc = $doc; + } + + public function prepare($xpath, array $valueMap) { + if (count($valueMap)==0) { + return $xpath; + } + foreach($valueMap as $key => $value) { + $xpath = str_replace(':'.$key, $this->quote($value), $xpath); + } + return $xpath; + } + + public function query($q, \DOMNode $ctx = null, $registerNodeNS = true) { + libxml_clear_errors(); + if (version_compare(PHP_VERSION, '5.3.3', '<') || strpos(PHP_VERSION, 'hiphop')) { + $rc = parent::query($q, ($ctx instanceof \DOMNode) ? $ctx : $this->doc->documentElement); + } else { + $rc = parent::query($q, ($ctx instanceof \DOMNode) ? $ctx : $this->doc->documentElement, $registerNodeNS); + } + + if (libxml_get_last_error()) { + throw new fDOMException('evaluating xpath expression failed.', fDOMException::QueryError); + } + return $rc; + } + + public function evaluate($q, \DOMNode $ctx = null, $registerNodeNS = true) { + libxml_clear_errors(); + if (version_compare(PHP_VERSION, '5.3.3', '<') || strpos(PHP_VERSION, 'hiphop')) { + $rc = parent::evaluate($q, ($ctx instanceof \DOMNode) ? $ctx : $this->doc->documentElement); + } else { + $rc = parent::evaluate($q, ($ctx instanceof \DOMNode) ? $ctx : $this->doc->documentElement, $registerNodeNS); + } + if (libxml_get_last_error()) { + throw new fDOMException('evaluating xpath expression failed.', fDOMException::QueryError); + } + return $rc; + } + + public function queryOne($q, \DOMNode $ctx = null, $registerNodeNS = true) { + $rc = $this->evaluate($q, $ctx, $registerNodeNS); + if ($rc instanceof \DOMNodelist) { + return $rc->item(0); + } + return $rc; + } + + public function quote($str) { + if (strpos($str, '"') === false) { + return '"'.$str.'"'; + } + $parts = explode('"', $str); + return 'concat("' . join('",\'"\',"', $parts).'")'; + } + } +} + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Arne Blankerts nor the names of contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @copyright Arne Blankerts , All rights reserved. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://github.com/theseer/fdomdocument + * + */ + +namespace TheSeer\fDOM { + + class XPathQueryException extends \Exception { + const KeyNotFound = 1; + } + +} + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Arne Blankerts nor the names of contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package fDOM + * @author Arne Blankerts + * @copyright Arne Blankerts , All rights reserved. + * @license BSD License + */ + +namespace TheSeer\fDOM { + + /** + * fDOMException + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @access public + * + */ + class fDOMException extends \Exception { + + const LoadError = 1; + const ParseError = 2; + const SaveError = 3; + const QueryError = 4; + const RegistrationFailed = 5; + const NoDOMXPath = 6; + const UnboundPrefix = 7; + const SetFailedError = 8; + const NameInvalid = 9; + + /** + * List of libxml error objects + * + * @var array + */ + private $errorList; + + + /** + * Full Error message + * + * @var string + */ + private $fullMessage = null; + + + /** + * Short Error Message + * + * @var string + */ + private $shortMessage = null; + + + private static $fullMesageMode = true; + + /** + * Constructor + * + * @param string $message Exception message + * @param integer $code Exception code + * @param Exception $chain optional chained exception + * + */ + public function __construct($message, $code = 0, \Exception $chain = NULL) { + $this->shortMessage = $message; + $this->errorList = libxml_get_errors(); + libxml_clear_errors(); + parent :: __construct($message, $code, $chain); + + $this->fullMessage = $message."\n\n"; + + foreach ($this->errorList as $error) { + // hack, skip "attempt to load external pseudo error" + if ($error->code=='1543') { + continue; + } + + if (empty($error->file)) { + $this->fullMessage .= '[XML-STRING] '; + } else { + $this->fullMessage .= '['.$error->file.'] '; + } + + $this->fullMessage .= '[Line: '.$error->line.' - Column: '.$error->column.'] '; + + switch ($error->level) { + case LIBXML_ERR_WARNING: + $this->fullMessage .= "Warning $error->code: "; + break; + case LIBXML_ERR_ERROR: + $this->fullMessage .= "Error $error->code: "; + break; + case LIBXML_ERR_FATAL: + $this->fullMessage .= "Fatal Error $error->code: "; + break; + } + + $this->fullMessage .= str_replace("\n", '', $error->message)."\n"; + + if (self::$fullMesageMode) { + $this->message = $this->fullMessage; + } + + } + } + + /** + * Accessor to fullMessage + * + * @return string + */ + public function getFullMessage() { + return $this->fullMessage; + } + + /** + * Access to shortMessage + * + * @return string + */ + public function getShortMessage() { + return $this->shortMessage; + } + + /** + * Accessor to errorList objets + * + * @return array + */ + public function getErrorList() { + return $this->errorList; + } + + /** + * Toggle wehter getMessage() should return full or only exception message + * + * @param boolean $full Flag to enable or disable full message output + * + * @return void + */ + public function toggleFullMessage($full = true) { + $this->message = $full ? $this->fullMessage : $this->shortMessage; + } + + /** + * Magic method for string context + * + * @return string + */ + public function __toString() { + return $this->fullMessage; + } + + } // fDOMException + +} + '/css/DollarEqualRule.php', + 'theseer\\fdom\\css\\notrule' => '/css/NotRule.php', + 'theseer\\fdom\\css\\nthchildrule' => '/css/NthChildRule.php', + 'theseer\\fdom\\css\\regexrule' => '/css/RegexRule.php', + 'theseer\\fdom\\css\\ruleinterface' => '/css/RuleInterface.php', + 'theseer\\fdom\\css\\translator' => '/css/Translator.php', + 'theseer\\fdom\\fdomdocument' => '/fDOMDocument.php', + 'theseer\\fdom\\fdomdocumentfragment' => '/fDOMDocumentFragment.php', + 'theseer\\fdom\\fdomelement' => '/fDOMElement.php', + 'theseer\\fdom\\fdomexception' => '/fDOMException.php', + 'theseer\\fdom\\fdomnode' => '/fDOMNode.php', + 'theseer\\fdom\\fdomxpath' => '/fDOMXPath.php', + 'theseer\\fdom\\xpathquery' => '/XPathQuery.php', + 'theseer\\fdom\\xpathqueryexception' => '/XPathQueryException.php' + ); + } + $cn = strtolower($class); + if (isset($classes[$cn])) { + require __DIR__ . $classes[$cn]; + } + } +); +// @codeCoverageIgnoreEnd + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Arne Blankerts nor the names of contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @copyright Arne Blankerts , All rights reserved. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://github.com/theseer/fdomdocument + * + */ + +namespace TheSeer\fDOM { + use TheSeer\fDOM\CSS\Translator; + + /** + * fDOMDocument extension to PHP's DOMDocument. + * This class adds various convenience methods to simplify APIs + * It is set to final since further extending it would even more + * break the Object structure after use of registerNodeClass. + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @access public + * + */ + class fDOMDocument extends \DOMDocument { + + /** + * XPath Object instance + * + * @var fDOMXPath + */ + private $xp = NULL; + + /** + * List of registered prefixes and their namespace uri + * @var Array + */ + private $prefixes = array(); + + /** + * Extended DOMDocument constructor + * + * @param string $version XML Version, should be 1.0 + * @param string $encoding Encoding, defaults to utf-8 + * @param array $streamOptions optional stream options array + * + * @return fDOMDocument + */ + public function __construct($version = '1.0', $encoding = 'utf-8', $streamOptions = NULL) { + if (!is_null($streamOptions)) { + $this->setStreamContext($streamOptions); + } + + libxml_use_internal_errors(TRUE); + $rc = parent::__construct($version, $encoding); + + $this->registerNodeClass('DOMDocument', get_called_class()); + $this->registerNodeClass('DOMNode', 'TheSeer\fDOM\fDOMNode'); + $this->registerNodeClass('DOMElement', 'TheSeer\fDOM\fDOMElement'); + $this->registerNodeClass('DOMDocumentFragment', 'TheSeer\fDOM\fDOMDocumentFragment'); + + return $rc; + } + + /** + * Reset XPath object so the clone gets a new instance when needed + */ + public function __clone() { + $this->xp = new fDOMXPath($this); + foreach($this->prefixes as $prefix => $uri) { + $this->xp->registerNamespace($prefix, $uri); + } + } + + public function __toString() { + return $this->C14N(); + } + + /** + * Set Stream context options + * + * @param Array $options Stream context options + * + * @return boolean true on success, false on failure + */ + public function setStreamContext(Array $options) { + if (!count($options)) { + return FALSE; + } + $context = stream_context_create($options); + libxml_set_streams_context($context); + return TRUE; + } + + /** + * Wrapper to DOMDocument load with exception handling + * Returns true on success to satisfy the compatibilty of the original DOM Api + * + * @param string $fname File to load + * @param int|null $options LibXML Flags to pass + * + * @throws fDOMException + * + * @return bool|mixed + */ + public function load($fname, $options = LIBXML_NONET) { + $this->xp = NULL; + $tmp = parent :: load($fname, $options); + if (!$tmp || libxml_get_last_error()) { + throw new fDOMException("loading file '$fname' failed.", fDOMException::LoadError); + } + return TRUE; + } + + /** + * Wrapper to DOMDocument loadXML with exception handling + * Returns true on success to satisfy the compatibilty of the original DOM Api + * + * @param string $source XML source code + * @param integer $options LibXML option flags + * + * @throws fDOMException + * + * @return boolean + */ + public function loadXML($source, $options = LIBXML_NONET) { + $this->xp = NULL; + $tmp = parent :: loadXML($source, $options); + if (!$tmp || libxml_get_last_error()) { + throw new fDOMException('parsing string failed', fDOMException::ParseError); + } + return TRUE; + } + + /** + * Wrapper to DOMDocument loadHTMLFile with exception handling. + * Returns true on success to satisfy the compatibilty of the original DOM Api + * + * @param string $fname html file to load + * @param integer $options Options bitmask (@see DOMDocument::loadHTMLFile) + * + * @throws fDOMException + * + * @return boolean + */ + public function loadHTMLFile($fname, $options = NULL) { + $this->xp = NULL; + if (version_compare(PHP_VERSION, '5.4.0', '<')) { + if ($options != NULL) { + throw new fDOMException('Passing options requires PHP 5.4.0+', fDOMException::LoadError); + } + $tmp = parent :: loadHTMLFile($fname); + } else { + $tmp = parent :: loadHTMLFile($fname, $options); + } + if (!$tmp || libxml_get_last_error()) { + throw new fDOMException("loading html file '$fname' failed", fDOMException::LoadError); + } + return TRUE; + } + + /** + * Wrapper to DOMDocument loadHTML with exception handling + * Returns true on success to satisfy the compatibilty of the original DOM Api + * + * @param string $source html source code + * @param integer $options Options bitmask (@see DOMDocument::loadHTML) + * + * @throws fDOMException + * + * @return boolean + */ + public function loadHTML($source, $options = NULL) { + $this->xp = NULL; + if (version_compare(PHP_VERSION, '5.4.0', '<')) { + if ($options != NULL) { + throw new fDOMException('Passing options requires PHP 5.4.0+', fDOMException::LoadError); + } + $tmp = parent :: loadHTML($source); + } else { + $tmp = parent :: loadHTML($source, $options); + } + if (!$tmp || libxml_get_last_error()) { + throw new fDOMException('parsing html string failed', fDOMException::ParseError); + } + return TRUE; + } + + /** + * Wrapper to DOMDocument::save with exception handling + * + * @param string $filename filename to save to + * @param integer $options Options bitmask (@see DOMDocument::save) + * + * @throws fDOMException + * + * @return integer bytes saved + */ + public function save($filename, $options = NULL) { + $tmp = parent::save($filename, $options); + if (!$tmp) { + throw new fDOMException('saving xml file failed', fDOMException::SaveError); + } + return $tmp; + } + + /** + * Wrapper to DOMDocument::saveHTML with exception handling + * + * @param DOMNode|null $node Context DOMNode (optional) + * + * @throws fDOMException + * @return string html content + */ + public function saveHTML(\DOMNode $node = NULL) { + if (version_compare(PHP_VERSION, '5.3.6', '<') && $node != NULL) { + throw new fDOMException('Passing a context node requires PHP 5.3.6+', fDOMException::SaveError); + } + $tmp = parent::saveHTML($node); + if (!$tmp) { + throw new fDOMException('serializing to HTML failed', fDOMException::SaveError); + } + return $tmp; + } + + /** + * Wrapper to DOMDocument::saveHTMLfile with exception handling + * + * @param string $filename filename to save to + * @param integer $options Options bitmask (@see DOMDocument::saveHTMLFile) + * + * @throws fDOMException + * + * @return integer bytes saved + */ + public function saveHTMLFile($filename, $options = NULL) { + $tmp = parent::saveHTMLFile($filename, $options); + if (!$tmp) { + throw new fDOMException('saving to HTML file failed', fDOMException::SaveError); + } + return $tmp; + } + + /** + * Wrapper to DOMDocument::saveXML with exception handling + * + * @param \DOMNode $node node to start serializing at + * @param integer $options options flags as bitmask + * + * @throws fDOMException + * + * @return string serialized XML + */ + public function saveXML(\DOMNode $node = NULL, $options = NULL) { + try { + $tmp = parent::saveXML($node, $options); + if (!$tmp) { + throw new fDOMException('serializing to XML failed', fDOMException::SaveError); + } + return $tmp; + } catch (\Exception $e) { + if (!$e instanceof fDOMException) { + throw new fDOMException($e->getMessage(), fDOMException::SaveError, $e); + } + throw $e; + } + } + + /** + * get Instance of DOMXPath Object for current DOM + * + * @throws fDOMException + * + * @return fDOMXPath + */ + public function getDOMXPath() { + if (is_null($this->xp)) { + $this->xp = new fDOMXPath($this); + } + if (!$this->xp) { + throw new fDOMException('creating DOMXPath object failed.', fDOMException::NoDOMXPath); + } + return $this->xp; + } + + /** + * Convert a given DOMNodeList into a DOMFragment + * + * @param \DOMNodeList $list The Nodelist to process + * @param boolean $move Signale if nodes are to be moved into fragment or not + * + * @return fDOMDocumentFragment + */ + public function nodeList2Fragment(\DOMNodeList $list, $move=FALSE) { + $frag = $this->createDocumentFragment(); + /** @var fDOMNode $node */ + foreach($list as $node) { + $frag->appendChild($move ? $node : $node->cloneNode(TRUE)); + } + return $frag; + } + + /** + * Perform an xpath query + * + * @param String $q query string containing xpath + * @param \DOMNode|null $ctx (optional) Context DOMNode + * @param boolean $registerNodeNS Register flag pass through + * + * @return \DOMNodeList + */ + public function query($q, \DOMNode $ctx = NULL, $registerNodeNS = TRUE) { + if (is_null($this->xp)) { + $this->getDOMXPath(); + } + return $this->xp->evaluate($q, $ctx, $registerNodeNS); + } + + /** + * Perform an xpath query and return only the 1st match + * + * @param String $q query string containing xpath + * @param \DOMNode $ctx (optional) Context DOMNode + * @param boolean $registerNodeNS Register flag pass thru + * + * @return fDOMNode + */ + public function queryOne($q, \DOMNode $ctx = NULL, $registerNodeNS = TRUE) { + if (is_null($this->xp)) { + $this->getDOMXPath(); + } + return $this->xp->queryOne($q, $ctx, $registerNodeNS); + } + + /** + * Forwarder to fDOMXPath's prepare method allowing for easy and secure + * placeholder replacement comparable to sql's prepared statements + * . + * @param string $xpath String containing xpath with :placeholder markup + * @param array $valueMap Array containing keys (:placeholder) and value pairs to be quoted + * + * @return string + */ + public function prepareQuery($xpath, array $valueMap) { + if (is_null($this->xp)) { + $this->getDOMXPath(); + } + return $this->xp->prepare($xpath, $valueMap); + } + + /** + * Use a CSS Level 3 Selector string to query select nodes + * + * @param string $selector A CSS Level 3 Selector string + * @param \DOMNode $ctx + * @param bool $registerNodeNS + * + * @return \DOMNodeList + */ + public function select($selector, \DOMNode $ctx = NULL, $registerNodeNS = TRUE) { + $translator = new Translator(); + $xpath = $translator->translate($selector); + if ($ctx != NULL) { + $xpath = '.' . $xpath; + } + return $this->query($xpath, $ctx, $registerNodeNS); + } + + /** + * Forward to DOMXPath->registerNamespace() + * + * @param string $prefix The prefix to use + * @param string $uri The uri to assign to this prefix + * + * @throws fDOMException + * + * @return void + */ + public function registerNamespace($prefix, $uri) { + if (is_null($this->xp)) { + $this->getDOMXPath(); + } + if (!$this->xp->registerNamespace($prefix, $uri)) { + throw new fDOMException("Registering namespace '$uri' with prefix '$prefix' failed.", fDOMException::RegistrationFailed); + } + $this->prefixes[$prefix] = $uri; + } + + /** + * Forward to DOMXPath->registerPHPFunctions() + * + * @param mixed $restrict Array of function names or string with functionname to restrict callabilty to + * + * @throws fDOMException + * + * @return void + */ + public function registerPHPFunctions($restrict = NULL) { + if (is_null($this->xp)) { + $this->getDOMXPath(); + } + $this->xp->registerPHPFunctions($restrict); + if (libxml_get_last_error()) { + throw new fDOMException("Registering php functions failed.", fDOMException::RegistrationFailed); + } + } + + /** + * Create a new element in namespace defined by given prefix + * + * @param string $prefix Namespace prefix for node to create + * @param string $name Name of not element to create + * @param string $content Optional content to be set + * @param bool $asTextNode Create content as textNode rather then setting nodeValue + * + * @throws fDOMException + * + * @return fDOMElement Reference to created fDOMElement + */ + public function createElementPrefix($prefix, $name, $content = NULL, $asTextNode = FALSE) { + if (!isset($this->prefixes[$prefix])) { + throw new fDOMException("'$prefix' not bound", fDOMException::UnboundPrefix); + } + return $this->createElementNS($this->prefixes[$prefix], $prefix.':'.$name, $content, $asTextNode); + } + + /** + * Create a new fDOMElement and return it, optionally set content + * + * @param string $name Name of node to create + * @param null $content Content to set (optional) + * @param bool $asTextNode Create content as textNode rather then setting nodeValue + * + * @throws fDOMException + * + * @return fDOMElement Reference to created fDOMElement + */ + public function createElement($name, $content = NULL, $asTextnode = FALSE) { + try { + $node = parent::createElement($name); + if (!$node) { + throw new fDOMException("Creating element with name '$name' failed", fDOMException::NameInvalid); + } + if ($content !== NULL) { + if ($asTextnode) { + $node->appendChild($this->createTextnode($content)); + } else { + $node->nodeValue = $content; + } + if (libxml_get_errors()) { + throw new fDOMException("Setting content value failed", fDOMException::SetFailedError); + } + } + } catch (\DOMException $e) { + throw new fDOMException("Creating elemnt with name '$name' failed", 0, $e); + } + return $node; + } + + /** + * Create a new fDOMElement within given namespace and return it + * + * @param string $namespace Namespace URI for node to create + * @param string $name Name of node to create + * @param null $content Content to set (optional) + * @param bool $asTextNode Create content as textNode rather then setting nodeValue + * + * @throws fDOMException + * + * @return fDOMElement + */ + public function createElementNS($namespace, $name, $content = NULL, $asTextNode = FALSE) { + $node = parent::createElementNS($namespace, $name); + if (!$node) { + throw new fDOMException("Creating element with name '$name' failed", fDOMException::NameInvalid); + } + if ($content !== NULL) { + if ($asTextNode) { + $node->appendChild($this->createTextnode($content)); + } else { + $node->nodeValue = $content; + } + if (libxml_get_errors()) { + throw new fDOMException("Setting content value failed", fDOMException::SetFailedError); + } + } + return $node; + } + + /** + * Check if the given node is in the same document + * + * @param \DOMNode $node Node to compare with + * + * @return boolean true on match, false if they differ + * + */ + public function inSameDocument(\DOMNode $node) { + if ($node instanceof \DOMDocument) { + return $this->isSameNode($node); + } + return $this->isSameNode($node->ownerDocument); + } + + /** + * Create a new element and append it as documentElement + * + * @param $name Name of not element to create + * @param $content Optional content to be set + * + * @return fDOMElement Reference to created fDOMElement + */ + public function appendElement($name, $content = NULL, $asTextNode = FALSE) { + return $this->appendChild( + $this->createElement($name, $content, $asTextNode) + ); + } + + /** + * Create a new element in given namespace and append it as documentElement + * + * @param $ns Namespace of node to create + * @param $name Name of not element to create + * @param $content Optional content to be set + * + * @return fDOMElement Reference to created fDOMElement + */ + public function appendElementNS($ns, $name, $content = NULL, $asTextNode = FALSE) { + return $this->appendChild( + $this->createElementNS($ns, $name, $content, $asTextNode) + ); + } + + } // fDOMDocument + +} + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Arne Blankerts nor the names of contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT * NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER ORCONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @copyright Arne Blankerts , All rights reserved. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://github.com/theseer/fdomdocument + * + */ + +namespace TheSeer\fDOM { + + /** + * fDOMDocumentFragment + * + * @category PHP + * @package TheSeer\fDOM + * @author Arne Blankerts + * @access public + * + */ + class fDOMDocumentFragment extends \DOMDocumentFragment { + + public function __toString() { + return $this->C14N(); + } + + /** + * Wrapper to standard method with exception support + * + * @param string $str Data string to parse and append + * + * @return boolean true on success + */ + public function appendXML($str) { + if (!parent::appendXML($str)) { + throw new fDOMException('Appending xml string failed', fDOMException::ParseError); + } + return true; + } + + /** + * Check if the given node is in the same document + * + * @param \DOMNode $node Node to compare with + * + * @return boolean true on match, false if they differ + * + */ + public function inSameDocument(\DOMNode $node) { + return $this->ownerDocument->inSameDocument($node); + } + + } // fDOMDocumentFragment + +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package PHP + * @subpackage Timer + * @author Sebastian Bergmann + * @copyright 2010-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @link http://github.com/sebastianbergmann/php-timer + * @since File available since Release 1.0.0 + */ + +/** + * Utility class for timing. + * + * @package PHP + * @subpackage Timer + * @author Sebastian Bergmann + * @copyright 2010-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @version Release: @package_version@ + * @link http://github.com/sebastianbergmann/php-timer + * @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']; +} + +else { + PHP_Timer::$requestTime = microtime(TRUE); +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package PHP_TokenStream + * @author Sebastian Bergmann + * @copyright 2009-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @since File available since Release 1.0.0 + */ + +/** + * A caching factory for token stream objects. + * + * @author Sebastian Bergmann + * @copyright 2009-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @version Release: @package_version@ + * @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(); + } + } +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package PHP_TokenStream + * @author Sebastian Bergmann + * @copyright 2009-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @since File available since Release 1.0.0 + */ + +/** + * A stream of PHP tokens. + * + * @author Sebastian Bergmann + * @copyright 2009-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @version Release: @package_version@ + * @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'; + } + + $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; + } + + else if ($tokenClass == 'PHP_Token_COMMENT' || + $tokenClass == 'PHP_Token_DOC_COMMENT') { + $this->linesOfCode['cloc'] += $lines + 1; + } + + if ($name == 'DOUBLE_COLON') { + $lastNonWhitespaceTokenWasDoubleColon = TRUE; + } + + else if ($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]; + } + + else if ($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 = FALSE; + $classEndLine = FALSE; + $trait = FALSE; + $traitEndLine = FALSE; + $interface = FALSE; + $interfaceEndLine = FALSE; + + foreach ($this->tokens as $token) { + switch (get_class($token)) { + case 'PHP_Token_HALT_COMPILER': { + return; + } + break; + + 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(); + $this->classes[$class] = $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 ($class === FALSE && + $trait === FALSE && + $interface === FALSE) { + $this->functions[$name] = $tmp; + + $this->addFunctionToMap( + $name, $tmp['startLine'], $tmp['endLine'] + ); + } + + else if ($class !== FALSE) { + $this->classes[$class]['methods'][$name] = $tmp; + + $this->addFunctionToMap( + $class . '::' . $name, + $tmp['startLine'], + $tmp['endLine'] + ); + } + + else if ($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 ($classEndLine !== FALSE && + $classEndLine == $token->getLine()) { + $class = FALSE; + $classEndLine = FALSE; + } + + else if ($traitEndLine !== FALSE && + $traitEndLine == $token->getLine()) { + $trait = FALSE; + $traitEndLine = FALSE; + } + + else if ($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 mixed $offset + */ + public function offsetExists($offset) + { + return isset($this->tokens[$offset]); + } + + /** + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + return $this->tokens[$offset]; + } + + /** + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + $this->tokens[$offset] = $value; + } + + /** + * @param mixed $offset + */ + public function offsetUnset($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('Invalid seek position'); + } + } + + private function addFunctionToMap($name, $startLine, $endLine) + { + for ($line = $startLine; $line <= $endLine; $line++) { + $this->lineToFunctionMap[$line] = $name; + } + } +} +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Sebastian Bergmann nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @package PHP_TokenStream + * @author Sebastian Bergmann + * @copyright 2009-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @since File available since Release 1.0.0 + */ + +/** + * A PHP token. + * + * @author Sebastian Bergmann + * @copyright 2009-2013 Sebastian Bergmann + * @license http://www.opensource.org/licenses/BSD-3-Clause The BSD 3-Clause License + * @version Release: @package_version@ + * @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 +{ + 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]; + } + } + + 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++; + } + + else if ($tokens[$i] instanceof PHP_Token_CLOSE_CURLY) { + $block--; + + if ($block === 0) { + $this->endTokenId = $i; + } + } + + else if (($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; + } + + public function getEndLine() + { + return $this->tokenStream[$this->getEndTokenId()]->getLine(); + } + +} + +abstract class PHP_TokenWithScopeAndVisibility extends PHP_TokenWithScope { + + 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; + } + } + } + + 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 +{ + protected $name; + protected $type; + + public function getName() + { + if ($this->name !== NULL) { + return $this->name; + } + + $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])) + ); + } + + return $this->name; + } + + public function getType() + { + $this->getName(); + return $this->type; + } +} + +class PHP_Token_REQUIRE_ONCE extends PHP_Token_Includes {} +class PHP_Token_REQUIRE extends PHP_Token_Includes {} +class PHP_Token_EVAL extends PHP_Token {} +class PHP_Token_INCLUDE_ONCE extends PHP_Token_Includes {} +class PHP_Token_INCLUDE extends PHP_Token_Includes {} +class PHP_Token_LOGICAL_OR extends PHP_Token {} +class PHP_Token_LOGICAL_XOR extends PHP_Token {} +class PHP_Token_LOGICAL_AND extends PHP_Token {} +class PHP_Token_PRINT extends PHP_Token {} +class PHP_Token_SR_EQUAL extends PHP_Token {} +class PHP_Token_SL_EQUAL extends PHP_Token {} +class PHP_Token_XOR_EQUAL extends PHP_Token {} +class PHP_Token_OR_EQUAL extends PHP_Token {} +class PHP_Token_AND_EQUAL extends PHP_Token {} +class PHP_Token_MOD_EQUAL extends PHP_Token {} +class PHP_Token_CONCAT_EQUAL extends PHP_Token {} +class PHP_Token_DIV_EQUAL extends PHP_Token {} +class PHP_Token_MUL_EQUAL extends PHP_Token {} +class PHP_Token_MINUS_EQUAL extends PHP_Token {} +class PHP_Token_PLUS_EQUAL extends PHP_Token {} +class PHP_Token_BOOLEAN_OR extends PHP_Token {} +class PHP_Token_BOOLEAN_AND extends PHP_Token {} +class PHP_Token_IS_NOT_IDENTICAL extends PHP_Token {} +class PHP_Token_IS_IDENTICAL extends PHP_Token {} +class PHP_Token_IS_NOT_EQUAL 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_SMALLER_OR_EQUAL extends PHP_Token {} +class PHP_Token_SR extends PHP_Token {} +class PHP_Token_SL extends PHP_Token {} +class PHP_Token_INSTANCEOF extends PHP_Token {} +class PHP_Token_UNSET_CAST extends PHP_Token {} +class PHP_Token_BOOL_CAST extends PHP_Token {} +class PHP_Token_OBJECT_CAST extends PHP_Token {} +class PHP_Token_ARRAY_CAST extends PHP_Token {} +class PHP_Token_STRING_CAST extends PHP_Token {} +class PHP_Token_DOUBLE_CAST extends PHP_Token {} +class PHP_Token_INT_CAST extends PHP_Token {} +class PHP_Token_DEC extends PHP_Token {} +class PHP_Token_INC extends PHP_Token {} +class PHP_Token_CLONE extends PHP_Token {} +class PHP_Token_NEW extends PHP_Token {} +class PHP_Token_EXIT extends PHP_Token {} +class PHP_Token_IF extends PHP_Token {} +class PHP_Token_ELSEIF extends PHP_Token {} +class PHP_Token_ELSE extends PHP_Token {} +class PHP_Token_ENDIF extends PHP_Token {} +class PHP_Token_LNUMBER extends PHP_Token {} +class PHP_Token_DNUMBER extends PHP_Token {} +class PHP_Token_STRING extends PHP_Token {} +class PHP_Token_STRING_VARNAME extends PHP_Token {} +class PHP_Token_VARIABLE extends PHP_Token {} +class PHP_Token_NUM_STRING extends PHP_Token {} +class PHP_Token_INLINE_HTML extends PHP_Token {} +class PHP_Token_CHARACTER extends PHP_Token {} +class PHP_Token_BAD_CHARACTER extends PHP_Token {} +class PHP_Token_ENCAPSED_AND_WHITESPACE extends PHP_Token {} +class PHP_Token_CONSTANT_ENCAPSED_STRING extends PHP_Token {} +class PHP_Token_ECHO extends PHP_Token {} +class PHP_Token_DO extends PHP_Token {} +class PHP_Token_WHILE extends PHP_Token {} +class PHP_Token_ENDWHILE extends PHP_Token {} +class PHP_Token_FOR extends PHP_Token {} +class PHP_Token_ENDFOR extends PHP_Token {} +class PHP_Token_FOREACH extends PHP_Token {} +class PHP_Token_ENDFOREACH extends PHP_Token {} +class PHP_Token_DECLARE extends PHP_Token {} +class PHP_Token_ENDDECLARE extends PHP_Token {} +class PHP_Token_AS extends PHP_Token {} +class PHP_Token_SWITCH extends PHP_Token {} +class PHP_Token_ENDSWITCH extends PHP_Token {} +class PHP_Token_CASE extends PHP_Token {} +class PHP_Token_DEFAULT extends PHP_Token {} +class PHP_Token_BREAK extends PHP_Token {} +class PHP_Token_CONTINUE extends PHP_Token {} +class PHP_Token_GOTO extends PHP_Token {} +class PHP_Token_CALLABLE extends PHP_Token {} +class PHP_Token_INSTEADOF extends PHP_Token {} + +class PHP_Token_FUNCTION extends PHP_TokenWithScopeAndVisibility +{ + protected $arguments; + protected $ccn; + protected $name; + protected $signature; + + public function getArguments() + { + if ($this->arguments !== NULL) { + return $this->arguments; + } + + $this->arguments = array(); + $tokens = $this->tokenStream->tokens(); + $typeHint = 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) { + $typeHint = (string)$tokens[$i]; + } + + else if ($tokens[$i] instanceof PHP_Token_VARIABLE) { + $this->arguments[(string)$tokens[$i]] = $typeHint; + $typeHint = NULL; + } + + $i++; + } + + return $this->arguments; + } + + 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; + } + + else if ($tokens[$i] instanceof PHP_Token_AMPERSAND && + $tokens[$i+1] instanceof PHP_Token_STRING) { + $this->name = (string)$tokens[$i+1]; + break; + } + + else if ($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; + } + + 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; + } + + 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_CONST extends PHP_Token {} +class PHP_Token_RETURN extends PHP_Token {} +class PHP_Token_YIELD extends PHP_Token {} +class PHP_Token_TRY extends PHP_Token {} +class PHP_Token_CATCH extends PHP_Token {} +class PHP_Token_FINALLY extends PHP_Token {} +class PHP_Token_THROW extends PHP_Token {} +class PHP_Token_USE extends PHP_Token {} +class PHP_Token_GLOBAL extends PHP_Token {} +class PHP_Token_PUBLIC extends PHP_Token {} +class PHP_Token_PROTECTED extends PHP_Token {} +class PHP_Token_PRIVATE extends PHP_Token {} +class PHP_Token_FINAL extends PHP_Token {} +class PHP_Token_ABSTRACT extends PHP_Token {} +class PHP_Token_STATIC extends PHP_Token {} +class PHP_Token_VAR extends PHP_Token {} +class PHP_Token_UNSET extends PHP_Token {} +class PHP_Token_ISSET extends PHP_Token {} +class PHP_Token_EMPTY extends PHP_Token {} +class PHP_Token_HALT_COMPILER extends PHP_Token {} + +class PHP_Token_INTERFACE extends PHP_TokenWithScopeAndVisibility +{ + protected $interfaces; + + public function getName() + { + return (string)$this->tokenStream[$this->id + 2]; + } + + public function hasParent() + { + return $this->tokenStream[$this->id + 4] instanceof PHP_Token_EXTENDS; + } + + 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; + } + + protected function arrayToName(array $parts, $join = '\\') + { + $result = ''; + + if (count($parts) > 1) { + array_pop($parts); + + $result = join($join, $parts); + } + + return $result; + } + + 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; + } + + 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); + } + + 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_CLASS extends PHP_Token_INTERFACE {} +class PHP_Token_CLASS_NAME_CONSTANT extends PHP_Token {} +class PHP_Token_TRAIT extends PHP_Token_INTERFACE {} +class PHP_Token_EXTENDS extends PHP_Token {} +class PHP_Token_IMPLEMENTS extends PHP_Token {} +class PHP_Token_OBJECT_OPERATOR extends PHP_Token {} +class PHP_Token_DOUBLE_ARROW extends PHP_Token {} +class PHP_Token_LIST extends PHP_Token {} +class PHP_Token_ARRAY extends PHP_Token {} +class PHP_Token_CLASS_C extends PHP_Token {} +class PHP_Token_TRAIT_C extends PHP_Token {} +class PHP_Token_METHOD_C extends PHP_Token {} +class PHP_Token_FUNC_C extends PHP_Token {} +class PHP_Token_LINE extends PHP_Token {} +class PHP_Token_FILE extends PHP_Token {} +class PHP_Token_COMMENT extends PHP_Token {} +class PHP_Token_DOC_COMMENT extends PHP_Token {} +class PHP_Token_OPEN_TAG extends PHP_Token {} +class PHP_Token_OPEN_TAG_WITH_ECHO extends PHP_Token {} +class PHP_Token_CLOSE_TAG extends PHP_Token {} +class PHP_Token_WHITESPACE extends PHP_Token {} +class PHP_Token_START_HEREDOC extends PHP_Token {} +class PHP_Token_END_HEREDOC extends PHP_Token {} +class PHP_Token_DOLLAR_OPEN_CURLY_BRACES extends PHP_Token {} +class PHP_Token_CURLY_OPEN extends PHP_Token {} +class PHP_Token_PAAMAYIM_NEKUDOTAYIM extends PHP_Token {} + +class PHP_Token_NAMESPACE extends PHP_TokenWithScope +{ + 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_DIR extends PHP_Token {} +class PHP_Token_NS_SEPARATOR extends PHP_Token {} +class PHP_Token_DOUBLE_COLON extends PHP_Token {} +class PHP_Token_OPEN_BRACKET extends PHP_Token {} +class PHP_Token_CLOSE_BRACKET extends PHP_Token {} +class PHP_Token_OPEN_SQUARE extends PHP_Token {} +class PHP_Token_CLOSE_SQUARE extends PHP_Token {} +class PHP_Token_OPEN_CURLY extends PHP_Token {} +class PHP_Token_CLOSE_CURLY extends PHP_Token {} +class PHP_Token_SEMICOLON extends PHP_Token {} +class PHP_Token_DOT extends PHP_Token {} +class PHP_Token_COMMA extends PHP_Token {} +class PHP_Token_EQUAL extends PHP_Token {} +class PHP_Token_LT extends PHP_Token {} +class PHP_Token_GT extends PHP_Token {} +class PHP_Token_PLUS extends PHP_Token {} +class PHP_Token_MINUS extends PHP_Token {} +class PHP_Token_MULT extends PHP_Token {} +class PHP_Token_DIV extends PHP_Token {} +class PHP_Token_QUESTION_MARK extends PHP_Token {} +class PHP_Token_EXCLAMATION_MARK extends PHP_Token {} +class PHP_Token_COLON extends PHP_Token {} +class PHP_Token_DOUBLE_QUOTES extends PHP_Token {} +class PHP_Token_AT extends PHP_Token {} +class PHP_Token_AMPERSAND extends PHP_Token {} +class PHP_Token_PERCENT extends PHP_Token {} +class PHP_Token_PIPE extends PHP_Token {} +class PHP_Token_DOLLAR extends PHP_Token {} +class PHP_Token_CARET extends PHP_Token {} +class PHP_Token_TILDE extends PHP_Token {} +class PHP_Token_BACKTICK extends PHP_Token {} +3rME2Xjx.)GBMB \ No newline at end of file diff --git a/build/bin/phploc-7.0.2.phar b/build/bin/phploc-7.0.2.phar new file mode 100644 index 0000000..bfa1e2d Binary files /dev/null and b/build/bin/phploc-7.0.2.phar differ diff --git a/build/bin/phpmd.phar b/build/bin/phpmd.phar new file mode 100644 index 0000000..1810de8 Binary files /dev/null and b/build/bin/phpmd.phar differ diff --git a/build/composer.json b/build/composer.json new file mode 100644 index 0000000..4d656f8 --- /dev/null +++ b/build/composer.json @@ -0,0 +1,8 @@ +{ + "config": { + "bin-dir": "bin/" + }, + "require": { + "pdepend/pdepend": "^2.9" + } +} diff --git a/command_usefull b/command_usefull new file mode 100644 index 0000000..f1aeff8 --- /dev/null +++ b/command_usefull @@ -0,0 +1,46 @@ +# PHP METRICS +#composer global require 'phpmetrics/phpmetrics' +phpmetrics --report-html=./data/phpmetrics ./ --exclude="vendor","build","tests","data","console/skel","application/modules","application/include/vues/cache" + +# PHP DOCUMENTOR +php ./build/bin/phpDocumentor.phar -d ./application -d ./console/ -d ./domain --ignore "vendor/*,build/*,data/*,tests/*,console/skel/*,application/modules/*,application/inculde/vues/cache/*" -t ./data/api-docs/ + +# PHP MESS DETECTOR +php ./build/bin/phpmd.phar ./ html codesize,design,naming,unusedcode --exclude '*vendor*' --exclude '*tests*' --exclude '*build*' --exclude '*data*' --exclude '*skel*' --exclude '*modules*' --exclude '*cache*' > data/phpmd.html + +# CHARTS OF PROJECTS +./build/bin/pdepend --jdepend-chart=data/jdepend-chart.svg --overview-pyramid=data/jdepend-overview.svg --summary-xml=data/jdepend-summary.xml --ignore=vendor,tests,build,data,console/skel,application/modules,application/inculde/vues/cache ./ + +# TEXT METRIC OF PROJECT +php ./build/bin/phploc-7.0.2.phar ./ --exclude ./vendor --exclude ./build --exclude ./tests --exclude ./data --exclude ./console/skel --exclude ./application/modules --exclude ./application/include/vues/cache > data/phploc.txt + +# DUPLICATED LINES OF PROJECT +php ./build/bin/phpcpd-6.0.3.phar --exclude ./vendor --exclude ./build --exclude ./build --exclude ./data --exclude ./console/skel --exclude ./application/modules --exclude ./application/include/vues/cache ./ > data/phpcpd.txt + +# DEAD CODE DETECTOR +php ./build/bin/phpdcd-1.0.2.phar --exclude="./vendor" --exclude="./build" --exclude="./build" --exclude="./data" --exclude="./console/skel" --exclude="./application/modules" --exclude="./application/include/vues/cache" --recursive ./ > data/phpdcd.txt + + + +# BEHAT Behavior Driven Developpement Initialisation +#cd tests/behat +#./bin/behat --init + +# PHPUNIT Testing +./tests/phpunit/bin/phpunit ./test/phpunit/tests/Tests.php + + + +# GENERATION OF DOCUMENTATION BOOK +php ./data/book sand print + + + + +# OBFUSCATION DU CODE (not working at all) +#git clone https://github.com/pk-fr/yakpro-po.git +#cd yakpro-po +#git clone https://github.com/nikic/PHP-Parser.git +#chmod a+x yakpro-po.php + +php ./yakpro-po/yakpro-po.php --no-shuffle-statements --no-obfuscate-namespace-name --no-obfuscate-class-name --no-obfuscate-method-name --no-obfuscate-property-name --no-obfuscate-constant-name --no-obfuscate-string-literal --no-strip-indentation './MyAwesomeProjectDirectory' -o './MyAwesomeObfuscatedProjectDirectory' diff --git a/console/bin.php b/console/bin.php index ee63210..d7589f1 100644 --- a/console/bin.php +++ b/console/bin.php @@ -20,7 +20,29 @@ if (isset($argv[1])) { \MVC\Classe\Logger::logCommandErrors($errors); } } else { - print "Command not found !\n"; + $command_file = dirname(__FILE__) . DIRECTORY_SEPARATOR . "command" . DIRECTORY_SEPARATOR . "app" . DIRECTORY_SEPARATOR. ucfirst($option[0]) . ".php"; + if (is_file($command_file)) { + $class = "\MVC\Command\\App\\" . ucfirst($option[0]); + $static_method = $option[1]; + $errors = $class::$static_method(); + + if ($errors !== null) { + \MVC\Classe\Logger::logCommandErrors($errors); + } + }else { + $command_file = dirname(__FILE__) . DIRECTORY_SEPARATOR . "command" . DIRECTORY_SEPARATOR . "sand" . DIRECTORY_SEPARATOR. ucfirst($option[0]) . ".php"; + if (is_file($command_file)) { + $class = "\MVC\Command\\Sand\\" . ucfirst($option[0]); + $static_method = $option[1]; + $errors = $class::$static_method(); + + if ($errors !== null) { + \MVC\Classe\Logger::logCommandErrors($errors); + } + }else { + print "Command not found !\n"; + } + } } } else { print "No command was specified !\n"; diff --git a/console/command/app/Log.php b/console/command/app/Log.php new file mode 100644 index 0000000..67236ea --- /dev/null +++ b/console/command/app/Log.php @@ -0,0 +1,38 @@ +modify('-1 month'); + $olderThan = $olderThan->format('Y-m-d'); + + //connection à la base de données + /*$bdd = new \MVC\Classe\Bdd('bdd1'); + $sql = "DELETE FROM logs WHERE date_creation < {$olderThan};"; + $bdd->faireSQLRequete($sql);*/ + + print "Log older than $nbMonth month cleared ! \n\n"; + } +} diff --git a/console/command/Action.php b/console/command/sand/Action.php similarity index 94% rename from console/command/Action.php rename to console/command/sand/Action.php index 83115f7..fea3fbf 100644 --- a/console/command/Action.php +++ b/console/command/sand/Action.php @@ -1,10 +1,10 @@ + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/data/doc-prince-book-generation/README.md b/data/doc-prince-book-generation/README.md new file mode 100644 index 0000000..27c8d97 --- /dev/null +++ b/data/doc-prince-book-generation/README.md @@ -0,0 +1,31 @@ +# Livre sur le Développement piloté par le comportement + +![](https://repository-images.githubusercontent.com/13304945/f5f26600-1e9b-11eb-84e7-fbbf90790e49) + +## Télécharger les ebooks + ++ 📗 **Tome 1 : à destination des personnes "métier"** ([pdf](https://github.com/Halleck45/livre-developpement-pilote-comportement/releases/download/1.0.0/developpement-pilote-par-le-comportement-tome1.pdf) | [epub](https://github.com/Halleck45/livre-developpement-pilote-comportement/releases/download/1.0.0/developpement-pilote-par-le-comportement-tome1.epub)) ++ 📙 **Tome 2 : pour les développeurs** ([pdf](https://github.com/Halleck45/livre-developpement-pilote-comportement/releases/download/1.0.0/developpement-pilote-par-le-comportement-tome2.pdf) | [epub](https://github.com/Halleck45/livre-developpement-pilote-comportement/releases/download/1.0.0/developpement-pilote-par-le-comportement-tome2.epub)) + +## Générer les livres locallement + +```./book --dir=doc publish ``` + +Par exemple: + +```./book --dir=doc publish tome1 print``` + +## Copyright + +Ce livre est publié avec EasyBook. + +Copyright (c) 2013 Jean-François Lépine. See LICENSE for details. + + +[![Licence Creative Commons](http://i.creativecommons.org/l/by-nc-sa/3.0/88x31.png)](http://creativecommons.org/licenses/by-nc-sa/3.0/deed.fr) +Le Développement piloté par le comportement de [Jean-François Lépine](http://communiquez.lepine.pro) est mis à disposition selon les termes de la [licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé](http://creativecommons.org/licenses/by-nc-sa/3.0/deed.fr). +Fondé(e) sur une œuvre à [https://github.com/Halleck45/livre-developpement-pilote-comportement](https://github.com/Halleck45/livre-developpement-pilote-comportement). + +## Contributeur + ++ Auteur: Jean-François Lépine (Halleck45, http://blog.lepine.pro) diff --git a/data/doc-prince-book-generation/book b/data/doc-prince-book-generation/book new file mode 100644 index 0000000..8dbe99c --- /dev/null +++ b/data/doc-prince-book-generation/book @@ -0,0 +1,34 @@ +#!/usr/bin/env php + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (!file_exists(__DIR__.'/vendor/autoload.php')) { + throw new \RuntimeException( + "\n" + ."[ERROR] easybook depends on some external libraries and components.\n" + ."It seems that those dependencies aren't properly installed.\n\n" + ."Perhaps you forgot to execute 'php composer.phar install' before\n" + ."using easybook for the first time?\n\n" + ."This command requires that you have previously installed Composer.\n" + ."To do so, execute the following command:\n\n" + ." $ curl -s http://getcomposer.org/installer | php" + ."\n\n" + ); +} + +require __DIR__.'/vendor/autoload.php'; + +use Easybook\DependencyInjection\Application; +use Easybook\Console\ConsoleApplication; + +$app = new Application(); +$console = new ConsoleApplication($app); +$console->run(); \ No newline at end of file diff --git a/data/doc-prince-book-generation/composer.json b/data/doc-prince-book-generation/composer.json new file mode 100644 index 0000000..0e11b1f --- /dev/null +++ b/data/doc-prince-book-generation/composer.json @@ -0,0 +1,25 @@ +{ + "name": "halleck45/livre-bdd", + "version": "1.0.0", + + "repositories": { + "geshi": { + "type": "package", + "package": { + "name": "geshi/geshi", + "version": "1.0.8.10", + "dist": { + "url": "http://sourceforge.net/projects/geshi/files/geshi/GeSHi%201.0.8.10/GeSHi-1.0.8.10.zip/download", + "type": "zip" + } + } + } + }, + + + "require": { + "easybook/easybook": "dev-master" + }, + + "minimum-stability":"dev" +} diff --git a/data/doc-prince-book-generation/doc/sand/Contents/00.a. Configuration de départ.md b/data/doc-prince-book-generation/doc/sand/Contents/00.a. Configuration de départ.md new file mode 100644 index 0000000..90e017a --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/00.a. Configuration de départ.md @@ -0,0 +1,16 @@ +#Les étapes de configurations de SAND + +il vous faut modifier quelques valeurs dans les fichiers de configuration suivant + - `application/config/app-parameters.php.skel` + - `application/config/bdd-parameters.php.skel` + + et enlever l'extension `.skel` afin que l'autoload de composer puisse le prendre en compte + + Vous pouvez tester une application de base simple en générant le dossier vendor en vous mettant à la racine de l'application et en lançant + `composer update`. Si votre serveur est correctement configurer pour pointer dans le dossier public sur l'index, vous devriez obtenir une application de test avec les fonctionnalités de base. En triturant un peu les fichier contenus dans le dossier `application/include` vous devriez comprendre comment se contruit une application faites avec SAND. + + Les autres fichiers contenus dans config servent pour la branche dev, mais peuvent aussi servir pour votre application. + + Dans le cas où vous choissisez d'utiliser les conduits lors du développement de votre application, + vous devrez modifier le fichier `application/config/files/routing.yml`. + \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/00.b. Comment bien utiliser les urls.md b/data/doc-prince-book-generation/doc/sand/Contents/00.b. Comment bien utiliser les urls.md new file mode 100644 index 0000000..f0b9ed3 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/00.b. Comment bien utiliser les urls.md @@ -0,0 +1,17 @@ +#Comment bien utiliser les urls dans le framework SAND + +Les urls d'accès basiques sont du type : + + `www.domain.tld/{page}/{varname1}/{varvalue1}/{varname2}/{varvalue2}/` ... + +Les variables sont automatiquement transmises au contrôleur et à la vue par le moteur MVC du framework + +où {page} est le nom : + +- Du contrôleur contenu dans `/application/include/controlleurs/{page}.php` +- Du modèle contenu dans `/application/include/modeles/{page}.model` +- De la vue contenue dans `/application/include/vues/view/{page}.blade.php` ou `/application/include/vues/view/{page}.html.twig` suivant le moteur de rendu + +Vous pouvez ainsi récupérer les variables passées en GET dans le contrôlleur depuis `$url_params['varname1']` + +D’autres types d’urls peuvent être mises en place par la configuration du routing symfony et des conduits, il faut alors renseigner les informations de routage dans le fichier `/application/config/files/routing.yml` et faire correspondre l’url d’accès au Conduit. diff --git a/data/doc-prince-book-generation/doc/sand/Contents/00.c. Comment bien utiliser les commandes console.md b/data/doc-prince-book-generation/doc/sand/Contents/00.c. Comment bien utiliser les commandes console.md new file mode 100644 index 0000000..c5eda82 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/00.c. Comment bien utiliser les commandes console.md @@ -0,0 +1,18 @@ +#Comment bien utiliser les commandes dans le framework SAND + +Les commandes console du framework sont toutes documentée par l'appel de la méthode help + +Par exemple pour voir les méthodes associés à la commande page vous pourvez lancer la commande suivante depuis le dossier console: + +`php bin.php page:help` + +et vous verrez les différentes méthodes qui y sont associées. + +Typiquement sur un projet on peut soit ajouter les fichiers manuellement quand on a l'habitude, soit aussi profiter des commandes: +`action`,`conduit`,`module` et `page` + +Si vous avez a videz le cache des vues et des logs vous pouvez utiliser la commande +`php bin.php cache:clear` +quand a +`php bin.php cache:stabilize` +vous permet de vider le cache des vues blade et/ou twig \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/00.d. Comment est architecturé le framework.md b/data/doc-prince-book-generation/doc/sand/Contents/00.d. Comment est architecturé le framework.md new file mode 100644 index 0000000..4925bb4 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/00.d. Comment est architecturé le framework.md @@ -0,0 +1,41 @@ +#Comment est architecturé le framework SAND + +Ce framework est nommé SAND pour l'acronyme récursif (Simplement [AND et] un Autre Nouveau Dossier), + +La racine contient les dossiers suivant : + +- `application` : dossier contenant différents fichiers, propre à l’application et au framework dont on détaillera le contenu plus bas. +- `console` : dossier contenant les commandes console propres au framework et à l’application +- `data` : dossier contenant des données qui sont chargé par l’applications ou le framework +- `docs` : dossier contenant la documentation de l’application +- `domain` : dossier contenant les classes propres à la manipulation du domaine visé par l’application, ces classes peuvent être testées unitairement par PHPUnit +- `output` : dossier contenant les sorties des commandes console de l’applications, ce peut être des logs ou des fichiers de traitement +- `public` : dossier contenant la racine publique de l’application, le serveur apache doit pointer sur ce dossier pour que le reste ne soit pas accessible, il contient tous les assets (css, js) de l’application ainsi que le fichier index.php et .htaccess redirigant toutes les requêtes vers l’index. +- `tests` : dossier contenant les fichiers de tests unitaire ou fonctionnels +- `vendor` : dossier créé par composer lors de la récupération des paquets nécessaire à l’application, contient aussi l’autoloader de l’application généré à la volée par composer + +Le dossier `application` contient les dossiers suivants: + +- `class` : dossier contenant les fichiers propres au framework. Ne doit pas être modifié. +- `config` : dossier contenant les fichiers de configuration, normalement lors de la récupération du dépôt, les fichiers sont a renommer et a ancrer avec les bonnes valeurs +- `include` : dossier de développement contient : + - `actions` : dossier contenant les actions réutilisables dans les vues de l’application + - `conduits` : dossier contenant le contrôleur qui est conduit par le routage symfony, ici utilisé exclusivement pour générer des réponses AJAX + - `controlleurs` : dossier contenant les contrôleurs de l’applications, nous verrons plus tard le modèles MVC propre au framework, mais il faut savoir que les controlleurs contiennent les accès en bdd de la page correspondant à son nom + - `modeles` :dossier contenant les modèles de l’application, il faut savoir que les modèles ont l’extension .model et contiennent les variable propres à la page, comme le title, la description ou plus récemment le fil d’ariane + - `vues` : dossier contenant les fichiers blade ou twig utilisé dans les pages, les actions et les conduits dont voici le détail : + - `cache` : dossier contenant les fichiers de cache généré par le moteur de rendu + - `layout` : dossier contenant les layout des pages + - `system` : dossier contenant le layout systeme + - `view` : dossier contenant toutes les vues de l’application, une convention de nommage peut être établie suivant le domaine de l’application, par exemple, toutes les vues des actions doivent commencer par action et toutes les vues des conduits doivent commencer par le nom du conduit duquel ils sont appelés +- `logs` : dossier contenant les logs générer par l’application, une classe dédiée aux logs est contenue dans le framework +- `modules` : dossier contenant les modules de l’application, il faut savoir que le framework peut contenir des applications modulaires, genre des applications symfony, wordpress, etc… toutes applications externe php peut avec un peu de doigté peut être intégré à une application, typiquement on peut intégrer un module de dépôt git genre gitlist à l’application que l’on est en train de développer. +- `objets` : dossier contenant les objets de session du framework, ce dossier peut être modifié par l’utilisateur, il contient des classes qui sont utilisé dans le cadre de la gestion de session, par exemple l’authentification CAS +- `traitements` : dossier contenant les traitements de formulaire selon le respect du pattern PRG (Post Redirect Get) + + +Le dossier `console` contient les dossiers suivants: + +- `command` : dossier contenant les commandes console du framework et de l’application +- `skel` : dossier contenant les squelettes utilisés lors des appels console, par exemple pour créer un nouvelle page vous pouvez utiliser la commande : +php bin.php page:add diff --git a/data/doc-prince-book-generation/doc/sand/Contents/00.e. Comment modifier proprement une page.md b/data/doc-prince-book-generation/doc/sand/Contents/00.e. Comment modifier proprement une page.md new file mode 100644 index 0000000..c1dcc35 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/00.e. Comment modifier proprement une page.md @@ -0,0 +1,20 @@ +#Comment modifier proprement une page contenu dans le framework SAND + +Tout d’abord il vous faut reconnaitre le nom de la page suivant le routage pour aller chercher les bons fichiers à modifier dans le dossier `include`. + +Par exemple : pour la page `index`, les bon fichiers à modifier sont : + +- `application/include/controlleurs/index.php` +- `application/include/modeles/index.model` +- `application/include/vues/view/index.blade.php` ou `application/include/vues/view/index.html.twig` + +Si vous désirez modifier le comportement d’une page il vous faudra très certainement modifier le contrôleur et la vue associée, voire peut-être l’action et sa vue si la vue mère fait des appels de ce type. + +Les actions se trouve dans le dossier `application/include/actions/` et leurs vues sont dans le même dossiers que les vues mères : `application/include/vues/view/`, il convient de la préfixer par `action-actionnamedescription`. + +Le `.model` de la page contient une variable engine qui peut prendre les valeurs `blade` ou `twig`. Par défaut si ce paramètre de modèle n’est pas renseigné c’est le moteur de rendu `blade` qui sera appelé. + +Si vous désirez juste modifier l’affichage, il vous suffira simplement de modifier la vue `blade` ou `twig` suivant le moteur de rendu. + +Cependant si vous désirez modifier le `head` ou le `layout` de la page vous devrez modifier les fichiers dans `application/include/vues/system/` ou `application/include/vues/layout/` + diff --git a/data/doc-prince-book-generation/doc/sand/Contents/00.f. Comment fonctionnent les objets de sessions.md b/data/doc-prince-book-generation/doc/sand/Contents/00.f. Comment fonctionnent les objets de sessions.md new file mode 100644 index 0000000..6d38e1e --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/00.f. Comment fonctionnent les objets de sessions.md @@ -0,0 +1,14 @@ +# Comment fonctionnent les objets de sessions + +le dossier `application/objets` contient les classes qui sont utilisés par les sessions, vous trouverez dans ce dossier les classes suivantes: + + - `Session.php` qui contient les méthodes permettant de créer les sessions, dans une application moderne il est courant de définir des droits sur les pages par un système d'authentification, ici SAND ne déroge pas à la règle et dans les fichiers .model possèdent une variable `authentification` qui peut prendre les valeurs `yes` ou `no`, ainsi le framework appelera automatiquement la méthode `createAndTestSession()` dans les controlleurs, si cette variable est défini à `no`, alors seule la méthode `sessionStart()` sera appelée, ceci afin que les alertes et l'historique puissent fonctionner. + Malheureusement les conduits et les traitements ne disposent pas d'un tel garde d'authentification, mais vous comprendrez qu'il est simple d'ajouter cette méthode au début d'un code de traitement ou de conduit. + + - `Alert.php` contient quelques méthodes simples qui utilise la variable `$_SESSION`, elle est vidée a chaque génération de page sauf dans le cas des traitements, ainsi automatiquement une alerte peut être définie à afficher sur la page de redirection + + - `History.php` permet d'obtenir à tout moment la page précédente de l'application, ainsi il est plus simple de rediriger les pages de traitement vers la page qui demande le traitement. Souvent dans les applications modernes vous avez un traitement qui doit être effectué depuis plusieurs pages cela permet de connaitre celle dont on provient sans passer par la variable serveur `$_SERVER["HTTP_REFERER"]` qui n'est pas toujours accessible. + + - `XssToken.php` permet de définir un token dans la variable `$_SESSION` afin de protéger les formulaires de l'application contre la faille XSS. + + Néanmoins il est possible que ce dossier contienne d'autres classes qui peuvent être utile, à vous de le fournir suivant les demandes de votre application. \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/00.g. Comment faire évoluer une application SAND.md b/data/doc-prince-book-generation/doc/sand/Contents/00.g. Comment faire évoluer une application SAND.md new file mode 100644 index 0000000..421c7fb --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/00.g. Comment faire évoluer une application SAND.md @@ -0,0 +1,7 @@ +#Comment faire évoluer une application écrite avec le framework SAND ? + +Tout d’abord, une fois l’application créé, toutes les ressources sont téléchargée par composer et le fichier composer.lock est garant de la stabilité des dépôts auxquels l’application fait appel. Donc normalement il ne sera pas nécessaire de faire évoluer ces ressources. + +Néanmoins passer vers une nouvelle version de php par exemple de la 5.6 à la 8 nécessite des connaissances évolué des fonctions dépréciées entre les versions de PHP. SAND framework est php 8 ready, néanmoins certaines lib ne fonctionne que sur php7.3, mais celles-ci une fois installée ne changeront pas au cours du temps car elles sont directement accessibles depuis le dossier vendor. + +Si vous utilisez les SPA vues.js, pensez à télécharger la version que vous utilisez afin de stabiliser votre application dans le temps, afin qu'elle ne consomme pas des ressources inutiles depuis le web. diff --git a/data/doc-prince-book-generation/doc/sand/Contents/01. Le controleur facile.md b/data/doc-prince-book-generation/doc/sand/Contents/01. Le controleur facile.md new file mode 100644 index 0000000..b2506ca --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/01. Le controleur facile.md @@ -0,0 +1,22 @@ +#Le contrôleur de base le plus simple possible + +il vous faut instancier deux fichiers sous cette forme: + +* application/include/modeles/name.model +``` +name : name +page_title : Page de l'application +description : Description de la page +params1 : {val1,val2} +params2 : val +``` +il faut absolument renseigner le name avec le nom générique de la page, page_title permet de modifier le contenu de la balise html title et description permet de modifier le contenu de la balise meta description. Enfin les parametres suivant sont optionnels et permet de passer des valeur dans le controlleur ou dans la vue. + +* application/include/controlleurs/name.php +```php +'blade', + "templating_b"=>'twig', + "templating_c"=>'edge' + ); +//recuperation des paramètres contenus dans le .model +$var[] = $params2; +foreach( $params1 as $key => $value ){ + $var[] = $value; +} +Logger::addLog('ok', 'Hello world'); +``` +tout en sachant que la variable `$templateData` est envoyé à la vue Blade + +* application/include/vues/view/name.blade.php +```php +@extends('body') + +@section('sidebar') + @parent +

This is appended to the master sidebar.

+@endsection + +@section('content') + Revenir a l'acceuil ? +
+ {{$templating_a}}::{{$templating_b}}::{{$templating_c}} + {{-- récupération des paramètres contenus dans le model --}} + {{$params2}} + @foreach ($param1 as $key => $value) + {{$key}} -> {{$value}} + @endforeach + +@endsection +``` +par exemple... + +--- + +Pour les instancier facilement vous pouvez utiliser la commande: +`php console/bin.php page:add` \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/03. Le controleur REST ou HTML.md b/data/doc-prince-book-generation/doc/sand/Contents/03. Le controleur REST ou HTML.md new file mode 100644 index 0000000..6c7fe74 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/03. Le controleur REST ou HTML.md @@ -0,0 +1,92 @@ +#Le contrôleur de REST / HTML + +>Ici le modèle (.model) n'est pas instancié, cela ressemble à symfony. +Étendre avec RESTResponse permet d'ajouter les methode get, put, post, delete +Étendre avec HttpResponse permet de n'avoir que les méthode Http qui sont utilisées + normalement avec tout php. + +>Donc vous pouvez tout a fait écrire une application avec HttpResponse car l'on peut appeler des vues blade par la méthode render(). +C'est un choix applicatif qu'il faut faire au moment de la contruction de l'application. + + +Vous devez instancier le fichier `application/include/controlleurs/{Name}HttpReponse.php` +qui est une classe peut implémenter `MVC\Classe\Implement\RestReponse` ou `MVC\Classe\Implement\HttpReponse` sachant que la différence se situe au niveau des méthodes qu'il doit instancier. + +##Voici un exemple avec `RestResponse` +```php +params); + Dumper::dump($this->data); + $text = ob_get_clean(); + Logger::addLog('put', '____Hello Put____' . PHP_EOL . $text); + } + public function delete() + { + ob_start(); + Dumper::dump($this->params); + Dumper::dump($this->data); + $text = ob_get_clean(); + Logger::addLog('delete', '____Hello Delete:____' . PHP_EOL . $text); + } + public function get() + { + ob_start(); + Dumper::dump($this->params); + Dumper::dump($this->data); + $text = ob_get_clean(); + Logger::addLog('get', '____Hello GET____' . PHP_EOL . $text); + } + public function post() + { + ob_start(); + Dumper::dump($this->params); + Dumper::dump($this->data); + $text = ob_get_clean(); + Logger::addLog('post', '____Hello POST____' . PHP_EOL . $text); + } + } +``` + +##voici un exemple avec `HttpResponse` +```php +params); + Dumper::dump($this->data); + $text = ob_get_clean(); + Logger::addLog('get', '____Hello GET____' . PHP_EOL . $text); + return $this->render('name', array('var' => $text)); + } + public function post() + { + ob_start(); + Dumper::dump($this->params); + Dumper::dump($this->data); + $text = ob_get_clean(); + Logger::addLog('post', '____Hello POST____' . PHP_EOL . $text); + return $this->render('name', array('var' => $text)); + } +} +``` + +l'accès se fait par l'url `http://monapp.local/{name}` \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/04.a. Les vues Twig.md b/data/doc-prince-book-generation/doc/sand/Contents/04.a. Les vues Twig.md new file mode 100644 index 0000000..bcdce07 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/04.a. Les vues Twig.md @@ -0,0 +1,65 @@ +#Les vues Twig + +Elles sont chargées dans cette ordre: + +* application/include/vues/system +* application/include/vues/layout +* application/include/vues/view + +ainsi la vue standard peut étendre de `application/include/vues/layout/body.html.twig` +```php + +{% block body %} + +
+
+ + + {% if $_SESSION['alerts'] is defined %} + {% foreach $_SESSION['alerts'] as alert %} + + {% endforeach %} + {% endif %} + + {% block content %}{% endblock %} + +
+
+{% endblock %}} + +``` +qui étends de `application/include/vues/system/system.html.twig` +```php + + + + {% block head %} + {{page_title}} + + + + + + + {% block top-css %} + + + {% endblock %} + {% endblock %} + + + +{% block top-javascript %}{% endblock %} + +{% block body %}{% endblock %} + +{% block bottom-javascript %} + + +{% endblock %} + + +``` \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/04.b. Les vues Blade.md b/data/doc-prince-book-generation/doc/sand/Contents/04.b. Les vues Blade.md new file mode 100644 index 0000000..31b602e --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/04.b. Les vues Blade.md @@ -0,0 +1,50 @@ +#Les vues Blade + +Elles sont chargées dans cette ordre: + +* application/include/vues/system +* application/include/vues/layout +* application/include/vues/view + +ainsi la vue standard peut étendre de `application/include/vues/layout/body.blade.php` +```php +@extends('system') + +@section('body') + + @section('sidebar') + This is the master sidebar. + @show + +
+ @yield('content') +
+ +@endsection +``` +qui étends de `application/include/vues/system/system.blade.php` +```php + + + + {{$page_title}} + + + @section('top-css') + @endsection + + + + +@section('top-javascript') +@endsection + +@yield('body') + +@section('bottom-javascript') +@endsection + + + + +``` \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/04.c. Les vues Blade avec SPA(vuejs).md b/data/doc-prince-book-generation/doc/sand/Contents/04.c. Les vues Blade avec SPA(vuejs).md new file mode 100644 index 0000000..34697e4 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/04.c. Les vues Blade avec SPA(vuejs).md @@ -0,0 +1,88 @@ +#Les vues Blade avec une SPA Vue.js + +Elles sont chargées dans le même ordre que les vues Blade normale: + +* application/include/vues/system +* application/include/vues/layout +* application/include/vues/view + +Cependant il faut inclure les biblitohèques Vue.js dans la page et coder à la mimine la SPA +par exemple: + +```php +@section('top-javascript') + @parent + + +@endsection + +@section('content') +

%PAGE% - VUE.js Controlleur

+


+ +@endsection + +@section('bottom-javascript') + @parent + +@endsection +``` \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/05. Les Actions dans les vues.md b/data/doc-prince-book-generation/doc/sand/Contents/05. Les Actions dans les vues.md new file mode 100644 index 0000000..d0b8cd2 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/05. Les Actions dans les vues.md @@ -0,0 +1,66 @@ +#Les actions +Celles-ci peuvent être appellé dans une vue par la méthode static qu'il convient d'instancier dans un fichier se trouvant dans le dossier `application/include/action/` +par exemple: +```php +render('action', array('var1' => $var1, 'var2' => $var2, 'var3' => $var3)); + } + public function variableSlug($data1,$data2) + { + /**your action algorythm**/ + + ob_start(); + print_r($data1); + echo "
"; + print_r($data2); + return ob_get_clean(); + } + public function makeHttp11() + { + $data = array('myval' => 25); + + $request = new HttpMethodRequete(); + $request->setUrl(Url::absolute_link_rewrite(false,'accueil',['var10'=>'val10']))->get($data); + $request->setUrl(Url::absolute_link_rewrite(false,'accueil',['var10'=>'val10']))->post($data); + $request->setUrl(Url::absolute_link_rewrite(false, 'accueil', ['var10' => 'val10']))->put($data); + $request->setUrl(Url::absolute_link_rewrite(false,'accueil',['var10'=>'val10']))->delete($data); + } +} +``` + +avec cet accès dans la vue: +```php + +{{\MVC\Classe\ControlleurAction::inserer('default',[])}} +{{\MVC\Classe\ControlleurAction::inserer('default.default',[4,5,6])}} +{{\MVC\Classe\ControlleurAction::inserer('default.variableSlug',['var1','var2'])}} + +{{\MVC\Classe\ControlleurAction::inserer('default.makeHttp11',[])}} +``` + +il faut absolument que l'action retourne du texte soit par la la méthode `render` soit par un `système de tampon` \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/06. Le conduit Symfony.md b/data/doc-prince-book-generation/doc/sand/Contents/06. Le conduit Symfony.md new file mode 100644 index 0000000..096d454 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/06. Le conduit Symfony.md @@ -0,0 +1,51 @@ +#Le conduit Symfony + +>Le conduit est une nouveautée de l'application, celle permet de réaliser +un controlleur similaire a Symfony qui est dénomé ici Conduit car il prend +en paramètre une route défini dans le fichier routing.yml. + +Cette fonctionnalité permet de choisir la route indépendamment du moteur. +il faut instancier le fichier `application/config/files/routing.yml` +avec la route de base et les routes que vous voulez conduire, par exemple: +```yml +home_route: + path: / + defaults: { controller: 'FooController::indexAction' } + +foo_route: + path: /foo + defaults: { controller: 'FooConduit::index' } + +foo_placeholder_route: + path: /foo/{id} + defaults: { controller: 'FooConduit::load' } + requirements: + id: '[0-9]+' +``` + +et définir le Conduit correspondant avec les méthodes correspondantes dans le dossier `application/include/conduits`, ici: +```php +render('foo', array('page_title' => 'Foo', 'description' => 'FooConduit')); + } + + // Route('/foo/{id}') + public function load() + { + echo "load of foo"; + return $this->render('foo', array('page_title' => 'Foo', 'description' => 'FooConduit', 'id' => $this->id)); + + } +} +``` +Vous remarquerez que les variables passé en GET sont obtenu dans le conduit par `$this->varname`. \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/07.a. modules - configuration.md b/data/doc-prince-book-generation/doc/sand/Contents/07.a. modules - configuration.md new file mode 100644 index 0000000..1a06eb0 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/07.a. modules - configuration.md @@ -0,0 +1,29 @@ +#Comment Configurer un module Symfony ou tout autre application php + +il vous faut instancier trois fichiers: +le modèle (.model) contenant le nom de la page qui porte le model +ici : `application/include/modeles/syf51.model` +```yaml +name : syf51 +page_title : Accueil de l'application modulaire +description : zatou stra bracadabla +params : params +``` +le controlleur (.php) contenant ce code qui doit être automatisé +ici: `application/include/controlleurs/syf51.php` +```php + $app); +``` +et déclarer le module dans `\application\modules\setup\registre.model` +par une ligne suplémentaire: +```yaml +syf51 : Application permettant de tester l'intégration d'un module avec symfony5.0.99 +``` + +si besoin et que le module n'existe pas il vous faudras coder et modifier +le fichier `/application/class/Modular.php` voir peut-être `/application/class/ModularRegister.php` + +Good Luck ! \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/07.b. modules - comment bien les utiliser.md b/data/doc-prince-book-generation/doc/sand/Contents/07.b. modules - comment bien les utiliser.md new file mode 100644 index 0000000..23e9e73 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/07.b. modules - comment bien les utiliser.md @@ -0,0 +1 @@ +#Comment bien utiliser les modules \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/08. modules - Wordpress.md b/data/doc-prince-book-generation/doc/sand/Contents/08. modules - Wordpress.md new file mode 100644 index 0000000..5551a5c --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/08. modules - Wordpress.md @@ -0,0 +1 @@ +#Comment ajouter un module Wordpress \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/09. modules - Prestashop.md b/data/doc-prince-book-generation/doc/sand/Contents/09. modules - Prestashop.md new file mode 100644 index 0000000..a2e3d46 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/09. modules - Prestashop.md @@ -0,0 +1 @@ +#Comment ajouter un module Prestashop \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/10. modules - PHPList.md b/data/doc-prince-book-generation/doc/sand/Contents/10. modules - PHPList.md new file mode 100644 index 0000000..7ab7154 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/10. modules - PHPList.md @@ -0,0 +1 @@ +#Comment ajouter un module PHPList \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/11. modules - GitList.md b/data/doc-prince-book-generation/doc/sand/Contents/11. modules - GitList.md new file mode 100644 index 0000000..6bd2788 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/11. modules - GitList.md @@ -0,0 +1 @@ +#Comment ajouter un module GitList \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/12. modules - Symfony.md b/data/doc-prince-book-generation/doc/sand/Contents/12. modules - Symfony.md new file mode 100644 index 0000000..6edc24e --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/12. modules - Symfony.md @@ -0,0 +1 @@ +#Comment ajouter un module Symfony \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/13. Comment faire des appels récursifs en curl sur une API RESTFULL codée avec le framework.md b/data/doc-prince-book-generation/doc/sand/Contents/13. Comment faire des appels récursifs en curl sur une API RESTFULL codée avec le framework.md new file mode 100644 index 0000000..3d21fd1 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/13. Comment faire des appels récursifs en curl sur une API RESTFULL codée avec le framework.md @@ -0,0 +1,48 @@ +# Comment ecrire une fonctionnalité récursive d'appel avec SAND + +Une fonctionnalité récursive d'appel permet d'appeler avec curl une api écrite avec le FrameWork SAND.* + +Typiquement on peut: + +- faire de multiples appel à l'api (REST ou Web) grace a curl dans un seul controlleur sans passez par javascript +- ainsi mieux découper les CRUD d'appel en bdd +- remplacer le pattern PRG, ou mieux le découper. + +C'est un choix applicatif a faire lors du developpement de l'application + +Il existe trois façon d'appeler un appel curl `HttpMethodRequest`, tous prennent en paramètre des valeurs passées dans un tableau: +``` +$data = array('a','b','c'); +``` +Voici la première: +``` +$request = new Response('http://myurl','myhttp1.1method'); +$request->addContent($data); +$request->send(); +``` +Voici la seconde: +``` +$request = new Response('http://myurl'); +``` +avec soit la création d'un context personnalisé +``` +$request->createContext('myhttp1.1method') +$request->addContent($data); +$request->send(); +``` +Soit les methodes HTTP1.1 +``` +$request->get($data); +$request->post($data); +$request->put($data); +$request->delete($data); +``` +Voici la dernière qui reste la plus facile à utiliser, +pour les développeurs qui comprennent les notations chainées: +``` +$request = new Response(); +$request->setUrl('http://myurl')->get($data) +$request->setUrl('http://myurl')->post($data) +$request->setUrl('http://myurl')->put($data) +$request->setUrl('http://myurl')->delete($data) +``` \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/BONUS - Comment troller efficacement.md b/data/doc-prince-book-generation/doc/sand/Contents/BONUS - Comment troller efficacement.md new file mode 100644 index 0000000..5bc4af9 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/BONUS - Comment troller efficacement.md @@ -0,0 +1,15 @@ +#BONUS - Comment troller efficacement +##prendre en compte l'aspect actuel de l'information +>en effet si absolument rendre compte que l'information n'est basé sur les faits divers et principalement sur l'actualité économique. En effet la plupart des médias ne parlent pas de l'information économique et informatique. En faisant cela vous devriez voir les niche qui doivent être visé par la trool attitude. Principalement parlez des faits divers ne nourris pas le trool. Et ne permet pas efficacement de nourrir une informations qui serait humoristique. Ainsi un troll d'humour serait basé sur des fait divers détourné et des sites comme le _Gorafi_ et l'_Echo de la boucle_ sont des niches a trool. + +##se mettre en avant de l'information visée en se basant sur des faits vécu +>en effet vous ne pouvez troller efficacement que sur des faits qui se rapporte a ce que vous avez lu en amont et que vous vivez actuellement par exemple si vous connaissez le monde des hackers et leurs déviance vous pouvez éviter les personnes frauduleuse mettant en avant des news dont vous pouvez mettre en garde au devant de touts les autres + +##se gardez de tout propos autre que ceux que vous mettez en avant +>avec des textes courts expliquant par des sources ce que vous visez vous pouvez mettre en avant votre informations vécue ainsi vous troller ceux qui n'ont pas vécu l’information dont vous parlez au moment de la lecture + +##attendre.. +>lorsque vous troller il se peut que l'information ne parvienne pas au oreille de ceux qui pourraient répondre. surtout si vous faites preuve de surréalisme. Ainsi vous expliquer ce que le pendant du monde réel as de pire et peut ainsi mettre en avant une expérience vécu. + +##encore attendre... +>si vous êtes progressiste il se peut que certaines personnes vous suivent afin de mieux comprendre ce que vous voulez dire dans ce texte que vous partager, n'oubliez surtou pas de fournir le plus de sources documentées et scientifiquement valable afin que votre article, brève, ou chronique soit effectivement une source d'information fiable. Ainsi ce troll devient une source mal écrit contenant des documents connexes ! \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Contents/BONUS - Les livres sur la programmation.md b/data/doc-prince-book-generation/doc/sand/Contents/BONUS - Les livres sur la programmation.md new file mode 100644 index 0000000..3c52dc5 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Contents/BONUS - Les livres sur la programmation.md @@ -0,0 +1,200 @@ +#BONUS - Quelques livres sur la programmation +* [Méta-listes](#méta-listes) +* [Non dépendant du langage](#non-dépendant-du-langage) + * [Algorithmique](#algorithmique) + * [Bases de données](#bases-de-données) + * [Logiciels libres](#logiciels-libres) + * [Makefile](#makefile) + * [Méthodes de développment](#méthodes-de-développement) + * [Systèmes informatiques](#systèmes-informatiques) + * [Théorie des langages](#théorie-des-langages) +* [Assembleur](#assembleur) +* [Bash / Shell](#bash--shell) +* [Caml](#caml) +* [C / C++](#c--c) +* [Coq](#coq) +* [CSS](#css) +* [Git](#git) +* [Haskell](#haskell) +* [Java](#java) +* [Javascript](#javascript) +* [LaTeX](#latex) + * [Asymptote](#asymptote) + * [Metapost](#metapost) + * [PGF/TikZ](#pgftikz) +* [Lisp](#lisp) +* [Perl](#perl) +* [PHP](#php) +* [Python](#python) +* [R](#r) +* [Ruby](#ruby) +* [Sage](#sage) +* [Scilab](#scilab) +* [SPIP](#spip) +* [TeX](#tex) +* [Vim](#vim) + +###Méta-listes + +* [Le SILO: Sciences du numérique & Informatique au Lycée: Oui!](https://wiki.inria.fr/sciencinfolycee/Accueil) + +###Non dépendant du langage + +####Algorithmique + +* [Algorithmique](http://pauillac.inria.fr/~quercia/cdrom/cours), par Michel Quercia +* [Éléments d'algorithmique](https://www.rocq.inria.fr/secret/Matthieu.Finiasz/teaching/ENSTA/IN101%20-%20poly%20algo.pdf) par Françoise Levy-dit-Vehel et Matthieu Finiasz +* [Éléments d'algorithmique](http://www-igm.univ-mlv.fr/~berstel/Elements/Elements.pdf) par D. Beauquier, J. Berstel, et Ph. Chrétienne +* [France-IOI](http://www.france-ioi.org/) +* [Prologin](http://www.prologin.org/) + +####Bases de données + +* [Bases de données I](http://decan.lexpage.net/files/bdd1/bdd1-syllabus.pdf), par Jef Wijsen + +####Logiciels libres + +* [Histoires et cultures du libres](http://framabook.org/histoires-et-cultures-du-libre/) +* [Option libre. Du bon usage des licences libres](http://framabook.org/option-libre-du-bon-usage-des-licences-libres/), par Jean Benjamin +* [Produire du logiciel libre](http://framabook.org/8-produire-du-logiciel-libre/), par Karl Fogel +* [Richard Stallman et la révolution du logiciel libre](http://framabook.org/richard-stallman-et-la-revolution-du-logiciel-libre/), par R.M. Stallman, S. Williams et C. Masutti + +####Makefile + +* [Concevoir un Makefile](http://icps.u-strasbg.fr/people/loechner/public_html/enseignement/GL/make.pdf), par Vincent Loechner d'après Nicolas Zin +* [Introduction aux Makefile](http://eric.bachard.free.fr/UTBM_LO22/P07/C/Documentation/C/make/intro_makefile.pdf) + +####Méthodes de développement + +* [Scrum et XP depuis les tranchées](http://www.infoq.com/resource/news/2007/06/scrum-xp-book/en/resources/ScrumAndXpFromTheTrenches_French.pdf), par Henrik Kniberg + +####Systèmes Informatiques + +* [Systèmes Informatiques (C, Unix/Linux,...)](http://sinf1252.info.ucl.ac.be/), par Olivier Bonaventure ([sources](https://github.com/obonaventure/SystemesInformatiques)) + +####Théorie des langages + +* [Compilation. Théorie des langages](http://www.lisyc.univ-brest.fr/pages_perso/leparc/Etud/Master/Compil/Doc/CoursCompilation.pdf) par Université de Bretagne Occidentale + +###Bash / Shell +* [Guide avancé d'écriture des scripts Bash](http://abs.traduc.org/abs-fr/) + +###Caml + +* [Introduction à Objective Caml](http://form-ocaml.forge.ocamlcore.org/html/index.html), par Maxence Guesdon +* [Le language Caml](http://caml.inria.fr/) + +###C / C++ + +* [Cours de C/C++](http://casteyde.christian.free.fr/cpp/cours/online/book1.html) par Christian Casteyde +* [Le C en 20 heures](http://framabook.org/6-le-c-en-20-heures/), par Eric Berthomier et Daniel Schang +* [Initiation à la programmation (en C++)](https://www.coursera.org/course/intro-cpp-fr), MOOC de l'École Polytechnique Fédérale de Lausanne +* [Introduction à la rétro-ingénierie de binaires](http://progdupeu.pl/articles/45/introduction-a-la-retro-ingenierie-de-binaires), à partir de code C compilé pour x86. +* [Programmation en C](https://www.rocq.inria.fr/secret/Matthieu.Finiasz/teaching/ENSTA/IN101%20-%20poly%20C.pdf) par Pierre-Alain Fouque et David Pointcheval + +###Coq + +* [Le Coq'Art (V8)](http://www.labri.fr/perso/casteran/CoqArt) par Yves Bertot et Pierre Castéran +* [Preuves de programmes en coq](http://fuscia.inrialpes.fr/cours/coq/) par Yves Bertot + +###CSS + +* [Apprendre les mises en page CSS](http://fr.learnlayout.com/) + +###Git + +* [Git Magic](http://www-cs-students.stanford.edu/~blynn/gitmagic/intl/fr/) par par Alexandre Garel, Paul Gaborit et Nicolas Deram +* [Pro Git](http://www.git-scm.com/book/fr) par Scott Chacon + +###Java + +* [Initiation à la programmation (en Java)](https://www.coursera.org/course/intro-java-fr), MOOC de l'École Polytechnique Fédérale de Lausanne + +###Javascript + +* [Javascript Éloquent : Une introduction moderne à la programmation](http://fr.eloquentjavascript.net/), par Marijn Haverbeke + +###Haskell + +* [Apprendre Haskell vous fera le plus grand bien !](http://lyah.haskell.fr/) +* [A Gentle Introduction to Haskell](http://gorgonite.developpez.com/livres/traductions/haskell/gentle-haskell/) par Paul Hudak, John Peterson et Joseph Fasel + +###LaTeX + +* [Détecter et résoudre +les problèmes](http://www.pearson.fr/livre/?GCOI=27440100048330), Annexe B du LaTeX Companion 2006, par Frank Mittelbach et Michel Goossens, mis à disposition par l'éditeur dans l'onglet « Compléments » +* [LaTeX... pour le prof de maths !](http://math.univ-lyon1.fr/irem/IMG/pdf/LatexPourProfMaths.pdf) par Arnaud Gazagnes +* [Tout ce que vous avez toujours voulu savoir sur LaTeX sans jamais oser le demander](http://framabook.org/5-tout-ce-que-vous-avez-toujours-voulu-savoir-sur-latex-sans-jamais-oser-le-demander/) par Vincent Lozano +* [(Xe)LaTeX appliqué aux sciences humaines](http://geekographie.maieul.net/95) par Maïeul Rouquette + +Voir aussi [TeX](#tex) + +####Asymptote + +* [Asymptote. Démarrage rapide](http://cgmaths.fr/cgFiles/Dem_Rapide.pdf), par Christophe Grospellier + +####Metapost + +* [Un manuel de Metapost](http://melusine.eu.org/syracuse/metapost/f-mpman-2.pdf), par John D. Hobby +* [Tracer des graphes avec Metapost](http://melusine.eu.org/syracuse/metapost/f-mpgraph.pdf), par John D. Hobby + +####PGF/TikZ + +* [TikZ pour l'impatient](http://math.et.info.free.fr/TikZ/), par Gérard Tisseau et Jacques Duma + +###Lisp + +* [Introduction à la programmation en Common Lisp](http://www.algo.be/logo1/lisp/intro-lisp.pdf) par Francis Leboutte +* [Traité de programmation en Common Lisp](http://dept-info.labri.fr/~strandh/Teaching/Programmation-Symbolique/Common/Book/HTML/programmation.html) par Robert Strandh et Irène Durand + +###Perl + +* [Guide Perl - débuter et progresser en Perl](http://formation-perl.fr/guide-perl.html), par Sylvain Lhullier +* [La documentation Perl en français](http://perl.mines-albi.fr/DocFr.html) + +###Php + +* [Cours de PHP 5](http://g-rossolini.developpez.com/tutoriels/php/cours/?page=introduction) par Guillaume Rossolini +* [Initiation au PHP](http://www.framasoft.net/IMG/pdf/initiation_php.pdf) par David Ducrocq +* [Programmer en PHP](http://www.lincoste.com/ebooks/pdf/informatique/programmer_php.pdf) par Julien Gaulmin + +###Python + +* [Appendre à programmer avec Python](http://inforef.be/swi/python.htm) par Gerard Swinnen +* [Dropbox a des fuites !Un aperçu de la rétro-ingénierie des programmes Python](http://progdupeu.pl/articles/34/dropbox-a-des-fuites) +* [Python](http://www.lincoste.com/ebooks/pdf/informatique/python.pdf) par Guido Van Rossum + +###R + +* [Introduction à la programmation en R](http://cran.r-project.org/doc/contrib/Goulet_introduction_programmation_R.pdf) par Vincent Goulet + +###Ruby + +* [Ruby en vingt minutes](https://www.ruby-lang.org/fr/documentation/quickstart/) +* [Venir à Ruby après un autre language](https://www.ruby-lang.org/fr/documentation/ruby-from-other-languages/) + +####Ruby on Rails + +* [Tutoriel Ruby on Rails : Apprendre Rails par l'exemple](http://french.railstutorial.org/chapters/beginning), par Michael Hartl + +###Sage + +* [Calcul mathématique avec Sage](http://sagebook.gforge.inria.fr/), par A. Casamayou, N. Cohen, G. Connan, T. Dumont, L. Fousse, F. Maltey, M. Meulien, M. Mezzarobba, C. Pernet, N. M. Thiéry, P. Zimmermann + +###Scilab + +* [Introduction à Scilab](http://forge.scilab.org/index.php/p/docintrotoscilab/downloads/) par Michaël Baudin, Artem Glebov, Jérome Briot + +###SPIP + +* [Programmer avec SPIP](http://programmer.spip.net/), par Matthieu Marcimat et collectif SPIP + +###TeX + +* [TeX pour l'Impatient](ftp://tug.org/tex/impatient/fr/fbook.pdf), par Paul Abrahams, Kathryn Hargreaves, and Karl Berry, trad. Marc Chaudemanche + +Voir aussi [LaTeX](#latex) + +###Vim +* [A Byte of Vim](http://swaroopch.com/notes/Vim_fr/) +* [Learn Vim Progressively](http://yannesposito.com/Scratch/fr/blog/Learn-Vim-Progressively/) diff --git a/data/doc-prince-book-generation/doc/sand/Resources/Templates/cover-small.jpg b/data/doc-prince-book-generation/doc/sand/Resources/Templates/cover-small.jpg new file mode 100644 index 0000000..988a180 Binary files /dev/null and b/data/doc-prince-book-generation/doc/sand/Resources/Templates/cover-small.jpg differ diff --git a/data/doc-prince-book-generation/doc/sand/Resources/Templates/cover.jpg b/data/doc-prince-book-generation/doc/sand/Resources/Templates/cover.jpg new file mode 100644 index 0000000..7ac1451 Binary files /dev/null and b/data/doc-prince-book-generation/doc/sand/Resources/Templates/cover.jpg differ diff --git a/data/doc-prince-book-generation/doc/sand/Resources/Templates/cover.pdf b/data/doc-prince-book-generation/doc/sand/Resources/Templates/cover.pdf new file mode 100644 index 0000000..df97ec9 Binary files /dev/null and b/data/doc-prince-book-generation/doc/sand/Resources/Templates/cover.pdf differ diff --git a/data/doc-prince-book-generation/doc/sand/Resources/Templates/print/style.css b/data/doc-prince-book-generation/doc/sand/Resources/Templates/print/style.css new file mode 100644 index 0000000..53f5b06 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Resources/Templates/print/style.css @@ -0,0 +1,83 @@ +body { + color: #333333; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + line-height: 20px; +} +p strong, li strong { + color:#008287; +} +.code { + background:#000; +} +.code { + color:#999; +} + +h1 { + +} +h2 { + background-color: #F7F7F9; + border: 1px solid #E1E1E8; + padding:10px; + margin-top:30px; +} +h3 { + font-size:14px !important; +} + +blockquote p { + font-size:18px; + background:#CCC; + color:#333; + width:80%; + font-style: italic; + margin-left:15%; + padding:10px; +} + +.item.toc .part.level-1 { + color:#F7F7F9; + background-color: #000; + border: 1px solid #E1E1E8; + padding:10px; + margin-top:30px; + font-size:26px; +} +.item.part h1 { + text-align:center; + font-size:70px; +} + + +/* --- gherkin --- */ +/*texte*/ +div.code.gherkin { + color:#C5C8C6; +} +/*commentaire*/ +div.code.gherkin .co1 { + color:#969896; +} +/*clef*/ +div.code.gherkin .co3 { + color:#DE935F; +} +/*Exemples*/ +div.code.gherkin .co4 { + color:#DE935F; +} +/*mot clef*/ +div.code.gherkin .kw1 { + color:#81A2BE; +} +/* +vert #B5BD68 +orange #DE935F +violet #B294BB +bleu #81A2BE +blanc #C5C8C6 +gris #969896 +jaune #B5BD68 +*/ \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/Resources/Templates/web/style.css b/data/doc-prince-book-generation/doc/sand/Resources/Templates/web/style.css new file mode 100644 index 0000000..f4f144a --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/Resources/Templates/web/style.css @@ -0,0 +1,63 @@ +body { + color: #333333; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + line-height: 20px; +} + +.code { + background:#000; +} +.code { + color:#999; +} + +h2 { + background-color: #F7F7F9; + border: 1px solid #E1E1E8; + padding:10px; + margin-top:30px; +} +h3 { + font-size:14px !important; +} + +em { + font-size:12px; + color:#c20cb9; +} +.item.part h1 { + text-align:center; + font-size:40px; +} + +/* --- gherkin --- */ +/*texte*/ +div.code.gherkin { + color:#C5C8C6; +} +/*commentaire*/ +div.code.gherkin .co1 { + color:#969896; +} +/*clef*/ +div.code.gherkin .co3 { + color:#DE935F; +} +/*Exemples*/ +div.code.gherkin .co4 { + color:#DE935F; +} +/*mot clef*/ +div.code.gherkin .kw1 { + color:#81A2BE; +} +/* +vert #B5BD68 +orange #DE935F +violet #B294BB +bleu #81A2BE +blanc #C5C8C6 +gris #969896 +jaune #B5BD68 +*/ \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/sand/config.yml b/data/doc-prince-book-generation/doc/sand/config.yml new file mode 100644 index 0000000..929c6c8 --- /dev/null +++ b/data/doc-prince-book-generation/doc/sand/config.yml @@ -0,0 +1,75 @@ +book: + title: "Le développement Avec Le Framework SAND" + subtitle: "Documentation d'utilisation de base" + author: "Emmanuel ROY" + edition: "First edition" + language: fr + publication_date: ~ + + generator: { name: easybook, version: 5.0-DEV } + + contents: + # available content types: acknowledgement, afterword, appendix, author, + # chapter, conclusion, cover, dedication, edition, epilogue, foreword, + # glossary, introduction, license, lof (list of figures), lot (list of + # tables), part, preface, prologue, title, toc (table of contents) + #- { element: cover } + #- { element: toc } + + #- { element: introduction, content: func-introduction.md } + - { element: chapter, number: 1, content: 00.a. Configuration de départ.md } + - { element: chapter, number: 2, content: 00.b. Comment bien utiliser les urls.md } + - { element: chapter, number: 3, content: 00.c. Comment bien utiliser les commandes console.md } + - { element: chapter, number: 4, content: 00.d. Comment est architecturé le framework.md } + - { element: chapter, number: 5, content: 00.e. Comment modifier proprement une page.md } + - { element: chapter, number: 6, content: 00.f. Comment fonctionnent les objets de sessions.md } + - { element: chapter, number: 7, content: 00.g. Comment faire évoluer une application SAND.md } + + + #- { element: author, content: author.md } + #- { element: license, content: license.md } + + + + editions: + ebook: + format: epub + highlight_code: false + include_styles: true + labels: ['appendix', 'chapter'] # labels also available for: "figure", "table" + theme: clean + toc: + deep: 1 + elements: ["appendix", "chapter", "part"] + + print: + format: pdf + highlight_code: true + include_styles: true + isbn: ~ + labels: ["appendix", "chapter"] # labels also available for: "figure", "table" + margin: + top: 25mm + bottom: 25mm + inner: 30mm + outter: 20mm + page_size: A4 + theme: clean + toc: + deep: 2 + elements: ["appendix", "chapter", "part"] + two_sided: true + + web: + format: html + highlight_code: true + include_styles: true + labels: ["appendix", "chapter"] # labels also available for: "figure", "table" + theme: clean + toc: + deep: 2 + elements: ["appendix", "chapter", "part"] + + website: + extends: web + format: html_chunked diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/author.md b/data/doc-prince-book-generation/doc/tome1/Contents/author.md new file mode 100644 index 0000000..a771593 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Contents/author.md @@ -0,0 +1,7 @@ +Je m'appelle Jean-François Lépine, et je suis consultant Web. Au quotidien, j'assiste les entreprises dans la conception et la réalisation de leurs applications web. + +Très impliqué dans l'éco-système OpenSource Web, j'anime le blog technique blog.lepine.pro. Vous pouvez retrouver quelques-unes de mes contributions sur mon [compte Github](http://www.github.com/Halleck45). + +Je suis également l'auteur du Mémento Industrialisation PHP : outils et qualité publié en 2012 aux éditions Eyrolles. + +Ces livres sont gratuits et open source. S'ils vous ont plu, n'hésitez pas à me le signaler sur [twitter](https://www.twitter.com/Halleck45) :-) diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/func-communiquez.md b/data/doc-prince-book-generation/doc/tome1/Contents/func-communiquez.md new file mode 100644 index 0000000..68470af --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Contents/func-communiquez.md @@ -0,0 +1,341 @@ +# Communiquez + +## Adoptez une Langue Commune + +Ce que vous entendez en utilisant un mot peut parfois (souvent !) être +totalement différent de ce qu'une autre personne entend pour ce même +mot. Et c'est encore pire avec les développeurs ! + + +Gardez cela à l'esprit : les développeurs ne parlent pas la même langue +que vous. Lorsque vous parlez "stock", "produit" ou "commande", un +développeur pensera "base de données", "enregistrement" et "sauvegarde +en base". + + +La première chose à faire est donc de constituer avec vos équipes une +Langue Commune. Peu importe le moyen : une feuille excel, un document +word ou un bout de papier feront l'affaire ! L'important  est de créer +un référentiel commun, souvent désigné par "Ubiquitus Language", +c'est-à-dire **un moyen d'utiliser une terminologie dont on sait qu'elle +est comprise par tous de la même manière**. + + +Attention, cet Ubiquitus Language, **cette Langue Commune ne doit PAS +être élaborée unilatéralement !** Instaurez un dialogue entre toutes les +parties prenantes du projet, jusqu'à déterminer des termes plus +importants que les autres. Identifiez les, puis, en commun, +définissez-les. Ne l'oubliez pas : une définition que vous aurez mis en +place tout seul ne voudra peut-être rien dire pour un développeur. + + +Dernière chose : ce référentiel commun ne doit porter que sur le domaine +fonctionnel. Il est inutile de créer un référentiel technique (du moins +pas en dehors des équipes techniques) : ce qui compte ce n'est pas de +faire du développement logiciel ; non, ce qui compte c'est votre +produit, vos bénéfices fonctionnels et votre vision ! + + +Pour synthétiser cette Langue Commune en quelques mots, elle permet : + ++ d'éviter un travail de traduction entre les tâches techniques et les +besoins fonctionnels (le jargon métier) ++ que tout le monde "parle" votre projet comme vous le "pensez" ++ d'éviter de vous retrouver avec des surprises idiotes, dues à des +incompréhensions sémantiques ou à des ambiguïtés. + + + +> les développeurs ne parlent pas la même langue que vous + + +## Laissez la technique aux développeurs + +Laissez-moi vous rassurez tout de suite : **non, vous n'avez pas besoin +de savoir comment fonctionne techniquement votre produit**. Non, vous +n'avez pas à comprendre la terminologie des développeurs. + +Mieux ! Vous ne devez SURTOUT PAS comprendre ce que font les +développeurs. + + +Ça fait maintenant quelques années que je conduis une voiture. Je sais +passer mes vitesses, je comprend comment lire mon tableau de bord... +Mais je ne sais pas, mais alors pas du tout, comment fonctionne ma +voiture. Et croyez-moi, je n'en ai rien à faire !  + + +Je dois vous avouer une chose : quand j'ai une panne, je vais voir mon +garagiste et je lui fais confiance. Quand il m'explique le problème, je +dis des "ah oui ?" avec un air curieux. Mais je ne comprend rien ! Et c'est +tant mieux ! + + +Pourquoi serait-ce différent pour l'informatique ? Mettre les mains +"dans la cambouis" risque : + ++ de brider vos demandes quand vous allez imaginer qu'un point va être +techniquement coûteux alors qu'il n'en n'est rien ++ de vous faire adopter une angle de vue différent (celui des développeurs) au détriment de votre vision initiale ++ de laisser le code dominer le domaine fonctionnel ++ de vous faire perdre votre force fonctionnel + +Je peux vous le dire : les projets les plus coûteux et les plus en +retard sur lesquels j'ai travaillé ont été initié par d'anciens +développeurs devenus chefs d'entreprise ou fonctionnels. Naturellement, +et on ne peut pas les en blâmer, ils ont tendance à vouloir comprendre +l'implémentation technique de leur produit.  + + +Cependant, cela est très coûteux : tout d'abord l'équipe technique doit +passer beaucoup de temps à expliquer ce qu'elle fait au lieu d'avancer +réellement. Ensuite, elle doit passer du temps également à monter en +compétence le fonctionnel. Enfin, et ce n'est pas le moindre, cela +introduira un sentiment de défiance entre vous et l'équipe technique, +sentiment qui va ralentir et limiter vos possibilités de créer une +Langue Commune et freiner l'investissement de vos équipes. + + +> Vous ne devez surtout pas comprendre ce que font les développeurs + + + +## Racontez votre produit + +Combien de documents sont passés entre vos mains sans que vous ne les +lisiez totalement, partiellement ou pas du tout ? Avouez-le, cela nous +est tous arrivé au moins une fois.  + +Pourquoi le document que vous fournissez à votre prestataire ne +serait-il pas justement celui-là même qu'il va survoler, ou mal +comprendre ? + +Un document word, par exemple, est une excellente base de travail. Vous +pouvez y noter vos réflexions, vous en servir comme brouillon... Mais +pas comme spécification ! + +Comprenez : votre pensée est ce qu'elle est : mouvante, changeante, et +surtout personnelle. Avec tous les efforts du monde, vous n'arriverez +pas à transmettre l'ensemble de votre pensée par écrit. Il subsistera +toujours de l'interprétation, des non-dits, de l'implicite... Voire même +parfois des contradictions qu'il faudra lever. C'est normal, comme un +projet, une pensée vit et change. + +Votre vision, vos besoins, vos spécifications... tout cela doit faire +l'objet d'un échange oral. Racontez votre produit plutôt que de le +décrire. Qu'en pensez-vous ? + +**Votre projet doit être le résultat d'une conversation** : impliquez +vous, mais impliquez aussi le testeur, le fonctionnel, l'ergonome, le +graphiste et le développeur. Racontez leur votre produit, et dialoguez, +de telle manière que : + ++ Ils **s'approprient votre besoin** ++ Ils **mettent en évidence certaines incohérences** le plus tôt possible ++ Ils vous proposent des changements (vous restez le seul maître de les +accepter ou non) ++ Ils soient impliqués et aient le sentiment d'être partie prenante de +votre projet ++ Ils ne perdent pas de temps à traduire votre message + +C'est d'ailleurs comme ça que vous allez vous rendre compte que certains +points demeurent obscurs, malgré tous vos efforts. Cela signifiera peut-être que +ces points sont plus importants que vous ne l'estimiez, et il sera +peut-être utile d'y consacrer plus de temps. + + +Oubliez donc les spécifications fonctionnelles détaillées (SFD), et +autres joyeusetés qui font la joie du développeur quand il doit lire un +document de 200 pages pour comprendre un besoin qu'il mettra 20 minutes +à implémenter. La documentation est utile, les SFG, SFD, etc. aussi, +mais uniquement quand elle est réservée aux cas qui le nécessitent +vraiment !  + + +Oubliez cette masse de documentation d'autant plus qu'elle va être un +frein à votre besoin de changements : très vite, avec de nombreux +documents, vous allez :  + ++ ou bien avoir un décalage conséquent entre vos spécifications et votre produit (quid des nouveaux arrivants sur le projet ?), ++ ou bien devoir passer un temps précieux à mettre à jour vos spécifications à chaque changement. Et ça, croyez-moi, dans la vraie vie ça n'arrive jamais si vous n'avez pas une ressource dédiée à cette tâche, à temps plein ! + + +Vous allez le voir, il existe d'autres moyens de préciser votre besoin. +En attendant, oubliez les documents word, pdf, rtf... Et Initiez la +conversation. + + +> Racontez votre produit plutôt que de le spécifier + +## Identifiez le Domaine fonctionnel + +Vous l'avez vu, vous avez besoin de constituer une Langue Commune avec +vos équipes. Vous le savez également, une image vaut souvent mieux qu'un +long discours... Pourquoi ne pas concilier les deux ? + +Il existe une approche, mise en évidence par Eric Evans, pour faire en +sorte le code source (ce qui permet à votre produit de fonctionner) +corresponde de près aux fonctionnalités métiers. Cette approche, c'est +le Domain Driven Design, ou Conception Pilotée par le Métier. + + +En général, au fil du temps, les développeurs qui auront travaillé sur +votre projet vont mettre en place ce qu'on appelle familièrement des +"rustines", des morceaux de code source rapides à mettre en place mais +peu maintenables, destinés à gérer vos demandes de changements fonctionnels. + + +Pire, dès le départ, les développeurs vont se focaliser sur les aspects +techniques du projet. Normal me direz-vous ? Non, car s'ils font cela, +l'énergie à dépenser pour comprendre les liens entre le code source et +le besoin fonctionnel risque d'être très importante, voire tellement +importante que votre projet risque de vous mettre sur la paille à la +moindre demande de changement. + +Le Domain Driven Design propose une approche différente : les +développeurs vont se focaliser avant tout sur le besoin métier, et +concevoir leur code source autour de ce besoin. Ça peut paraître une +évidence, mais c'est loin d'être le cas, croyez-moi ! Le code source +sera alors le reflet du besoin métier. + + +Si vous et vos équipes souhaitez mettre en place cette orientation (du +Domain Driven Design), **la première chose dont vous aurez besoin, après +une Langue Commune, sera une cartographie du domaine fonctionnel couvert +par votre application**.  + +Rassurez-vous, rien de bien compliqué. Oubliez les logiciels de +modélisation, oubliez Visio, oubliez l'UML, Merise (si vous les avez +déjà utilisés), voici venue l'ère ... du papier et du crayon ! + + +Listez, puis représentez sur un dessin simple, l'ensemble des grands +concepts fonctionnels liés à votre produit (ceux qui sont présents dans +votre glossaire, votre Langue Commune). Reliez-les entre eux par de +flèches annotées, qui représenteront les différentes interactions de ces +concepts entre eux. S'il y a trop de flèches à un endroit, c'est que +vous vous attardez trop sur des détails. Si vous pensez que ce ne sont +pas des détails, prenez une autre feuille et redémarrez, mais cette fois +en zoomant sur la partie concernée. + +Cette cartographie, qui doit être simple (en caricaturant un peu, disons +que votre neveu de huit ans devrait pouvoir la comprendre s'il connaît +votre Langue Commune !), sera le **référentiel du Comportement de votre +Produit**. + + +Ce comportement étant désormais identifié, il sera plus facile pour les +équipes d'appréhender les changements et d'en identifier les +conséquences. + + +Cette cartographie vous servira également : elle va vous permettre +d'identifier l'indispensable, l'utile et le pratique dans votre produit. +Vos équipes doivent commencer à travailler sur l'indispensable. +L'utile et le pratique viendront après. + + +> Prenez un papier et un crayon, puis dessinez la cartographie +fonctionnelle de votre Produit + + +## Faites-vous interviewer + +Mais attention, cette cartographie fonctionnelle, en réalité... ce n'est +pas à vous de la faire ! + +Pourquoi donc cet air surpris sur votre visage ? Je veux simplement dire que ce +n'est pas à vous *seul* de la réaliser. Mais rassurez-vous, vous en serez le +moteur. + +Concrètement, vous devez dans un premier temps discuter avec vos +développeurs, mais surtout ceux-ci doivent réaliser un travail d'enquête : +vos développeurs doivent vous interviewer.  + +Ou plutôt ils doivent interviewer le Produit. Après tout... vous êtes le +porte-parole de votre produit. + +Cet interview, comme une interview classique va consister en différents +moments : + ++ présentation générale d'un aspect fonctionnel ++ une question de l'équipe technique sur un point abordé ++ votre réponse fonctionnelle ++ une nouvelle question de l'équipe technique ++ etc. + + +Attention, l'interview ne doit jamais dériver sur des aspects +techniques. Il sera largement temps plus tard de voir comment résoudre +techniquement tel ou tel point. Pour l'instant, ne discutez QUE du +fonctionnel. + + +Cette démarche a au moins trois avantages : + ++ impliquer les équipes techniques sur votre besoin métier ++ mettre en évidence des aspects implicites, qui sont évidents pour une +personne du métier, mais totalement inconnus des équipes techniques ++ vous permettre de prendre du recul sur votre métier et votre besoin en +l'expliquant à des néophytes. + +> Les équipes techniques doient interviewer le porte-parole du produit + +## Les tâches ne servent à rien + +Vous voulez que vos développeurs développent une nouvelle fonctionnalité +? Ne leur donnez pas de tâche à faire ! N'exprimez que votre besoin... + +Imaginez un instant cette scène : vous re-voila chez votre +concessionnaire automobile, prêt à signer le chèque de votre nouvelle +voiture. A votre avis, des deux situations suivantes, laquelle est la +plus adaptée : + + ++ Situation 1 : + > Client : "J'ai besoin de la voiture le mois prochain. Il vous faudra + > donc passer commande auprès du constructeur. Pour cela vous disposez du + > logiciel mis à disposition par ce dernier. Entrez vos codes d'accès, + > puis saisissez la référence de la voiture, puis cliquez sur "Commander". + > Dès que vous recevez la voiture, faites un bilan complet, puis + > appelez-moi." + ++ Situation 2 : + > Client : "J'ai besoin de ma voiture le mois prochain, je sais que vous + > ferez le maximum. Tenez moi simplement régulièrement informé de + > l'avancée de la commande..." + + +Alors, à votre avis, quelle situation est la plus adaptée ? La deuxième +sans doute, non ? Examinons la Situation 1 : que se passe t-il si le +concessionnaire appelle directement le constructeur pour commander votre +voiture plutôt que de respecter les tâches qui lui sont assignées... + +Quel en sera le résultat ? + ++ Vous serez livré plus vite ++ **Les objectifs seront atteints** ++ **Pourtant le concessionnaire n'aura pas accompli les tâches demandées** + +Je pense que vous avez compris : peu importe ce que va faire votre +concessionnaire, ce qui compte c'est d'être livré à temps. Pire, vous +risquez d'embrouiller le concessionnaire ou de le mettre sur des fausses +pistes. Avec une liste de tâches à suivre, le concessionnaire ne +pourrait pas prendre l'initiative d'appeler directement le constructeur +automobile.  + + +Et bien pour le développeur c'est pareil. **Exprimez votre besoin, les +délais souhaités, les modalités, mais ne lui dite pas comment réaliser +ce besoin**, vous risquez de le mettre sur de fausses pistes. + + +Attention, les listes de tâches sont utiles bien entendus, mais pas pour +communiquer. Vous pouvez avoir votre liste de tâches, les équipes de +développement vont peut-être (sans doute !) transformer votre besoin en +sous-tâches techniques... Mais en aucun cas vous n'avez besoin de +communiquer avec des tâches à faire. Le dialogue ne doit porter que sur +votre besoin métier. + +> Communiquer en liste de tâches ne sert à rien, le dialogue ne doit +porter que sur le besoin métier. diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/func-conclusion.md b/data/doc-prince-book-generation/doc/tome1/Contents/func-conclusion.md new file mode 100644 index 0000000..cee5ddd --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Contents/func-conclusion.md @@ -0,0 +1,139 @@ +# Le mot de la fin + + +## Parlons la même langue + +Nous avons discuté des idées, des concepts... Il est temps d'établir +notre Langue Commune. + + + +**Produit** + +: Concept dont l'application sera à terme la réalisation. Ce concept vise +à rendre possible la Vision. + +**Vision** + +: Guide et oriente votre projet. Son but est de changer ce qui, dans la +vie de vos utilisateurs, peut les gêner, leur manquer ou les +insatisfaire. + +**Bénéfice fonctionnel** + +: Confort métier tiré par les utilisateurs de votre Produit.  + + +**Langue Commune** + +: Langage clair pour l'ensemble des acteurs d'un projet. Ce langage est +défini en commun. + +**Cartographie du Produit** + +: Représentation visuelle simple du Comportement de votre Produit. Cette +représentation peut concerner l'ensemble ou une partie de votre Produit. + +**Fonctionnalité** + +: Besoin métier spécifique. Elle est représentée par un texte court qui +communique ce besoin métier au reste de l'équipe. Elle concerne le +service rendu à un utilisateur, et décrit le bénéfice fonctionnel qui +en est tiré. + +**Scénario** + +: Illustration des situations dans lesquelles se manifeste une +Fonctionnalité. Elle est représentée par une situation initiale, un +événement et un résultat attendu. + +**Exemple** + +: Illustre un Scénario de manière concrète. Il enlève toute ambiguïté +possible sur une étape d'un scénario ou sur le scénario lui-même. + +**Livrable** + +: Application concrète livrée au client. Il délivre le Comportement +raconté par les Fonctionnalités et les Scénarios. Il est possible de +fournir un livrable régulièrement au cours de la vie du Projet. + +**Recette** + +: Opération qui contrôle que le Livrable correspond au Produit, en se +basant sur les Fonctionnalités et les Scénarios. Peut être automatisée +avec des outils spécialisés. + +## Cartographie du Développement piloté par le Comportement + +![ Cartographie du Développement piloté par le Comportement ](cartographie.png) + + +## Les 10 commandements pour vous mettre au Développement piloté par le Comportement + + +1) **Votre Vision est le seul guide du projet** + +: Chaque fonctionnalité, chaque ressource consacrée au projet, tout doit + servir cette Vision. Elle est le point de repère et la motivation de + chacun des acteurs de votre Produit.  + +2) **Créez une Langue Commune**. + +: Un développeur ne parle pas la même langue que vous. Créez un + référentiel et un vocabulaire commun ensemble, et utilisez la syntaxe + proposée par le Développement piloté par le Comportement, claire pour la + majorité des gens. + +3) **Concevez vos scénarios ensemble**. + +: C'est le seul moyen de retirer toutes les ambiguïtés qui peuvent exister + entre vous, votre produit et vos équipes. Ne laissez personne rédiger un + scénario tout seul. + +4) **Vos scénarios ne concernent pas l'interface graphique** + +: Faites abstraction du support matériel de votre Produit : site web, + application mobile... Ce qui compte vraiment c'est le comportement de + votre Produit. + +5) **L'automatisation des recettes fonctionnelles n'est qu'un bonus** + +: Automatiser des recettes fonctionnelles n'a de sens uniquement si au préalable + vous avez réussi à faire comprendre votre besoin. Sans dialogue ni + échanges, les tests automatisés testeront que l'application fait bien + quelque chose, mais pas forcément ce que vous attendez. + +6) **L'automatisation des tests techniques est primordiale** + +: Laissez le temps aux équipes techniques et poussez-les à écrire des + tests automatisés de code source (test unitaire). C'est le seul moyen + pour eux de réaliser vos demandes légitimes de changements sans faire + exploser vos coûts. + +7) **KISS : Keep It Simple and Stupid** + +: Gardez vos scénarios simples et explicites, et n'employez pas dix mots + quand cinq suffisent. Si votre scénario est long, c'est probablement + qu'il recouvre un domaine fonctionnel trop large : découpez-le. + +8) **Soyez conscient que vos scénarios changeront** + +: En confrontant régulièrement votre Produit aux avis des utilisateurs, + une grande partie de vos scénarios va changer, voire disparaître. + Acceptez-le, et faites en sorte que les équipes techniques l'acceptent + en les faisant participer à ce processus de changement. + +9) **Utilisez des exemples** + +: Vous ne rédigez pas des tests d'acceptation, mais établissez une + communication pour décrire votre besoin. Illustrez votre besoin par des + exemples, pourquoi pas avec humour, pour effacer toute ambiguïté.   + +10) **Le Développement piloté par le Comportement n'est pas une recette miracle** + +: La recette miracle n'existe pas. Mais c'est au moins un bonne démarche, + éprouvée, pour vous faire comprendre et faciliter la communication entre + fonctionnels et techniques. Elle aidera chacun des acteurs de votre + projet à comprendre son rôle et ce que vous attendez de lui ; il ne + reste plus à chacun qu'à être un bon acteur. diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/func-exprimez-le-besoin.md b/data/doc-prince-book-generation/doc/tome1/Contents/func-exprimez-le-besoin.md new file mode 100644 index 0000000..059c01c --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Contents/func-exprimez-le-besoin.md @@ -0,0 +1,351 @@ +# Exprimez votre besoin + + +## Tous la même grammaire + +Les spécifications classiques, on l'a vu, échouent souvent : elles +acceptent mal le changement, sont souvent lourdes et ne concernent pas +toujours que le besoin métier. + + +Pour remédier à cela, il est possible d'utiliser une autre approche, +celle préconisée par le Développement Piloté par le Comportement. + + +"Quel terme barbare !" me direz-vous. C'est vrai. Mais, en fait, rien de +technique ou de geek derrière ce terme. C'est simplement une démarche +qui consiste à communiquer avec vos développeurs d'une autre manière, de +sorte qu'il vous comprennent. + + +C'est cette démarche que je vous propose de découvrir à partir de +maintenant. + + +Cette approche a au moins trois avantages : + ++ L'écrit sert à la fois à spécifier et à échanger avec les équipes ++ Le changement est mieux géré ++ Vous savez à tout instant ce qui est fait et ce qu'il reste à faire + + +Pour au moins un inconvénient : + ++ L'investissement et la disponibilité du Propriétaire du Produit doivent +être importants + + +Cette approche se base sur la rédaction des spécifications sous un +formalisme différent, dénommé généralement Gherkin. Ce formalisme n'est +pas obligatoire, mais il a été prouvé à plusieurs reprises qu'il est +pratique et répond bien aux problématiques que vous rencontrerez +probablement. + + +Si vous souhaitez faire du Développement Piloté par le Comportement +(Behaviour Driven Development en anglais, ou BDD), chaque acteur de +votre projet doit apprendre à lire et à comprendre ce formalisme. Pas de +panique, le coût d'apprentissage est quasi nul ; c'est justement l'un +des nombreux atouts de Gherkin. + + +## Décrivez vos Fonctionnalités métiers + +Vous avez votre Vision du Produit, il est temps d'expliquer ce que vous +attendez aux développeurs. + + +Rien de plus simple : commencez par identifier les aspects fonctionnels +que vous souhaitez qu'ils développent, et ce en les décrivant avec une +courte phrase. Juste une phrase, courte, pas plus. Cette phrase sera le +titre de votre fonctionnalité. + + +**Chaque fonctionnalité doit pouvoir se distinguer des autres par son +titre**. Prenez les fonctionnalités suivantes : + + [gherkin] + # Distinguez vos fonctionnalités en leur donnant un titre : + Fonctionnalité : Retirer de l’argent au distributeur + Fonctionnalité : Consulter le solde de son compte au distributeur + +Il est facile de les distinguer et de les comprendre non ? + + +Identifier clairement ces fonctionnalités est important : cela vous +permettra de les prioriser plus facilement. + + +Bon, vous allez me dire qu'une petite phrase de rien du tout ça laisse la +place à beaucoup d'ambiguïtés ; et vous aurez bien raison ! + + +Vous le savez, un Produit n'est rien sans un utilisateur pour +l'utiliser. **Précisez donc qui est l'utilisateur de votre +fonctionnalité** : + + [gherkin] + # Précisez qui est l’acteur bénéficiaire de votre fonctionnalité : + En tant que rôle + En tant que administrateur du site + En tant que conseiller clientèle + En tant que ... + +Voilà une bonne chose de faite ! **Identifiez maintenant le Service +rendu par votre fonctionnalité**. Je conseille toujours de voir le +service comme ni plus ni moins que ce que votre commercial entend lorsqu'il +dit "c'est génial, en tant que ... vous pouvez faire ça !". Par exemple +: + + [gherkin] + # Identifiez le service délivré par votre fonctionnalité à l’utilisateur : + Je peux un service rendu + Je peux retirer de l’argent au distributeur + Je peux connaître la quantité d’argent qu’il me reste sur mon compte + Je peux ... + +On commence déjà à y voir plus clair. **Il ne reste plus qu'à préciser +le bénéfice métier** de votre fonctionnalité. Car oui, une +fonctionnalité sans bénéfice pour l'utilisateur ne sert à rien. Si vous +ne voyez pas de bénéfice pour votre fonctionnalité, c'est sans doute qu'elle ne +mérite pas d'être développée. Décrivez ce bénéfice ainsi : + + [gherkin] + # Décrivez le bénéfice fonctionnel obtenu par votre fonctionnalité : + De telle sorte que bénéfice attendu + De telle sorte que je puisse obtenir de l’argent même quand la banque est fermée + De telle sorte que ... + +Ce qui fait que **toute fonctionnalité peut être décrite, de manière +explicite, en quatre courtes lignes** : + + [gherkin] + Fonctionnalité : Retirer de l’argent au distributeur + En tant que client de la banque + Je peux retirer de l’argent au distributeur + De telle sorte que je puisse obtenir de l’argent même quand la banque est fermée + +N'importe quel membre de votre équipe comprendra ce qu'est cette +fonctionnalité. Faites attention à n'employer que le vocabulaire de votre +Langue Commune pour éviter toute ambiguïté. + + +## Des scénarios pour vos fonctionnalités + +Un titre, un rôle, le service rendu et le bénéfice métier c'est déjà pas +mal. Mais en général ça ne va pas suffire : vous pouvez avoir envie +d'illustrer vos fonctionnalités. + + +C'est le rôle des scénarios. **Chaque fonctionnalité peut contenir un +ensemble de scénarios. Ces scénarios doivent tous être clairs, uniques +et explicites**. Ils représentent le comportement concret que vous +attendez pour votre fonctionnalité. Par exemple : + + [gherkin] + # Un Scénario est une situation concrète de votre Fonctionnalité : + Fonctionnalité : Retirer de l’argent au distributeur + + Scénario : Retirer de l’argent avec une carte valide + Scénario : Retirer de l’argent avec une carte expirée + Scénario : Retirer de l’argent sur un distributeur qui ne contient + plus d’argent + Scénario : ... + +La réalisation (ou non) dans votre application de ces scénarios détermine l'avancée de votre +fonctionnalité. + + +Il est possible que ces scénarios évoluent : certains seront réalisés +tôt, d'autres seront réalisés plus tard ou seront modifiés... Dans tous +les cas, **jeter un oeil à l'avancée de ces scénarios vous permettra de +maîtriser l'avancée de votre fonctionnalité**. + + +Vous vous en doutez, il peut arriver qu'une simple phrase ne suffise pas +à décrire avec précision un scénario. Il sera alors temps d'y introduire +un contexte, un événement déclencheur ou un résultat attendu. + + +Le contexte du scénario est introduit par l'expression "étant donné". Il situe les +conditions d'existence de votre scénario : + + [gherkin] + # Un Scénario s’inscrit dans un contexte, une situation initiale : + Scénario : ... + Etant donné que tel contexte existe + Etant donné que que je n’ai plus d’argent sur mon compte + Etant donné que ma carte est expirée + Etant donné que ... + +Toute interaction de l'utilisateur avec votre produit peut se traduire +sous forme d'événement. Un événement est le déclencheur de votre +scénario, qui vient juste après que le contexte initial soit situé : + + [gherkin] + # Un Scénario se démarque par un événement : + Scénario : ... + ... + Quand tel événement déclencheur survient + Quand je demande à retirer de l’argent + Quand je demande le solde de mon compte + Quand ... + +Si chaque action a une conséquence, il en va de même pour les événements : +chaque événement attend un résultat, exprimé par l'expression "alors" : + + [gherkin] + # Chaque Scénario aboutit à un résultat : + Scénario : ... + ... + Alors tel résultat est attendu + Alors je reçois 20 euros + Alors ma carte est aspirée + Alors ... + +Toute cette syntaxe, qui permet de rendre vos fonctionnalités +explicites, sera comprise par vos développeurs. Et en plus elle n'est +pas très compliquée, si ? + + +Au final, voici une fonctionnalité plus complète : + + [gherkin] + # Toute personne qui connaître votre Langue Commune comprend + # votre fonctionnalité : + + Fonctionnalité : Retirer de l’argent au distributeur + En tant que client de la banque + Je peux retirer de l’argent au distributeur + De telle sorte que je puisse obtenir de l’argent + même quand la banque est fermée + + Scénario : Retirer de l’argent avec une carte valide et un + compte approvisionné + Etant donné que je détiens un compte bancaire soldé de 100 euros + Et que ma carte expire l’an prochain + Quand je demande à retirer 20 euros + Alors je reçois 20 euros + Et ma carte m’est restituée + + Scénario : Retirer de l’argent avec une carte expirée + Etant donné que ma carte est expirée + Quand je demande à retirer 20 euros + Alors ma carte est aspirée + + +## Illustrez le tout avec des exemples + +Ce formalisme est utile. Il n'est en aucun cas obligatoire bien entendu, +mais il a démontré son efficacité, et a l'énorme avantage d'être **clair +tout autant pour les fonctionnels que pour les développeurs.** + + +Maintenant, ce formalisme peut manquer de concret. Qu'à cela ne tienne, +vous pouvez y ajouter vos exemples. Car on est bien d'accords, **rien de +mieux que des exemples**. Prenons celui-ci : + + [gherkin] + # Votre fonctionnalité peut contenir un exemple : + + Scénario : Retirer de l’argent avec un compte approvisionné + Etant donné que ma carte expire l’an prochain + Quand je demande à retirer "20" euros + Alors je reçois "20" euros + +Bon, c'est simple, mais limité, car cela nous oblige à recopier le +scénario autant de fois que nous avons d'exemples. Heureusement, notre +grammaire est riche, et vous pouvez utiliser la syntaxe suivante (votre +scénario devient alors un Plan du scénario) :  + + [gherkin] + # Votre fonctionnalité peut contenir plusieurs exemples facilement : + + Plan du Scénario : Retirer de l’argent avec un compte approvisionné + Etant donné que ma carte expire l’an prochain + Quand je demande à retirer "" euros + Alors je reçois "" euros + + Exemples : + | montant-demandé | montant-reçu | + | 20 | 20 | + | 1000 | 1000 | + | 50 | 50 | + +Pratique non ? Les symboles < et \> indiquent une valeur d'exemple, à +rechercher dans le tableau juste en dessous. + +Le tableau est dessiné grâce au caractère "pipe". Si vous ne savez pas le situer +sur votre clavier, il suffit de taper `Alt Gr" + 6`, ou `Alt + Maj + L` si vous +utilisez un Mac. Rassurez-vous, comme vous le verrez par la suite, il existe des +outils graphiques pour vous faciliter la vie. + +## Travaillez en équipe + +Vous êtes maître de votre Produit, et c'est vous qui décidez de vos +Fonctionnalités. Par contre, n'oubliez pas que **le dialogue et +l'échange sont primordiaux pour éviter les ambiguïtés dans votre +projet**. + + +Lorsque vous allez rédiger vos scénarios, voici ce qui risque de se +passer : + ++ Vous allez devoir les expliquer à votre équipe technique lorsque vous les leur fournirez ++ Vous n'allez peut-être pas penser à tout, et vos équipes devront vous solliciter très (trop) régulièrement pour des aspects que vous aurez oubliés ++ Certains de vos scénarios ne seront pas clairs ou n'emploieront pas le vocabulaire de la Langue Commune (on a toujours tendance à se trouver soi-même très clair ; ce n'est pas toujours vrai...) ++ L'équipe risque de ne pas se sentir suffisamment impliquée + + +Ces écueils ne sont bien sûr pas systématiques, mais le risque est là. +Pourquoi prendre ce risque alors que la solution est simple ? + + +Il suffit en effet de faire participer toutes les équipes lors de la +rédaction des scénarios. Attention, il ne s'agit pas de laisser +n'importe qui faire n'importe quoi... Non, **c'est à vous d'expliquer +votre besoin**, dans la Langue Commune, et **ensuite vous allez ensemble +le traduire en scénarios**, de telle sorte que tous comprennent +clairement ce que vous attendez d'eux. + + +Pour résumer, écoutez les remarques de vos équipes lors de la rédaction +des scénarios. Idéalement, vous devriez entendre des "oui mais qu'est-ce +qui se passe si..." et "je ne comprend ce que devient...". Si c'est le +cas, le pari est réussi ! + + +Dans tous les cas, gardez vos scénarios clairs et concis. Personne n'a +envie de lire des scénarios de 20 ou 30 lignes... + + +> "Ecrivez vos scénarios avec l'équipe technique ; le temps passé sera +compensé par le gain de compréhension et l'inutilité d'avoir à faire des +va-et-vient incessants au cours du développement" + + +## Des assistants visuels + +Vous aviez déjà le vocabulaire (la Langue Commune), vous voici désormais +avec la grammaire nécessaire pour exprimer votre besoin. Mais on est +tous les mêmes : personne n'aime pas la grammaire ! + + +Après tout, si votre véritable objectif est de vous focaliser sur +l'expression de votre besoin, sur la description du comportement de +votre Produit, à quoi bon se concentrer sur la manière de l'exprimer ? + + +Pour vous simplifier la vie il existe un certain nombre d'outils qui +vont vous assister dans la  rédaction de vos Fonctionnalités. Ils vont +vous permettre de rédiger vos features selon une approche plus agréable, +tandis même qu'ils vont présenter à vos développeurs votre besoin sous +forme de fichiers clairs qu'ils pourront directement exploiter. + +![ Il existe des assistants visuels pour faciliter votre expression de besoin ](behat-wizard-edit.jpg) + +Il existe plusieurs outils, qui ont tous leurs avantages. On peut penser +par exemple à BehatViewer, ou à BehatWizard... N'hésitez pas à demander +vos équipes techniques de vous installer ces outils. + + diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/func-impliquez-les-parties-prenantes.md b/data/doc-prince-book-generation/doc/tome1/Contents/func-impliquez-les-parties-prenantes.md new file mode 100644 index 0000000..8111bb3 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Contents/func-impliquez-les-parties-prenantes.md @@ -0,0 +1,150 @@ +# Impliquez les parties prenantes + + +## Un projet est comme un Film + +Drôle de titre, n'est-ce pas ? Pourtant on peut le dire, un projet +informatique, c'est un peu comme un film : on peut prendre plaisir à le +tourner, y apprendre des choses, mais l'essentiel reste que ce film voit +le jour. + +Faisons un jeu : quel est le point commun à tous les films, films +d'animation, court-métrages ? Tic-tac, tic-tac... Driiing ! Réponse : +ils ont tous un générique, c'est à dire une liste des différentes +personnes qui y ont travaillé, classées par rôle. + +Et bien dans un projet informatique c'est pareil, il existe des rôles. +Il peut arriver que certaines personnes aient plusieurs rôles, selon la +taille du projet, mais il est indispensable de définir les domaines de +compétence nécessaires à la bonne tenue de votre projet. + +En général, voici les rôles qui seront associés à votre projet : + + +1. Le **Propriétaire du produit** : + Personne qui fournit la Vision du produit. Est souvent celui qui finance + le projet. + +2. Les **Alliés du produit** : + Personnes qui soutiennent et aident fonctionnellement le Propriétaire du + produit. Elles peuvent avoir un impact sur le Produit + +3. Les **Analystes fonctionnels** : + Personnes qui déterminent les solutions fonctionnelles à apporter pour + répondre à la Vision du Propriétaire dur Produit + +4. Les **Ergonomes** : + Personnes qui déterminent l'ergonomie la plus adaptée pour mettre en + place les solutions fonctionnelles auprès des utilisateurs ciblés par le + Produit + +5. Les **Graphistes** : + Personnes en charge d'améliorer l'aspect visuel ou d'appliquer des + solutions proposées par les Ergonomes + +6. Les **Testeurs** : + Personnes en charge de la recette applicative de premier niveau + + +7. Les **Développeurs** : + Personnes en charges de l'implémentation technique des solutions + proposées par les Analystes fonctionnels + + + +Bien, ça, c'est dit. Bien entendu, il est fréquent que certains de ces +rôles se chevauchent, le plus souvent pour des raisons de coût ou de ressources : +l'ergonome est graphiste, l'analyste fonctionnel est propriétaire du +produit, etc. + + + +Dans tous les cas, ces rôles doivent dialoguer entre eux, puisque chacun +a son rôle à jouer dans la réalisation du Produit. Gardez ce découpage +en tête, il est important pour la suite. + + + +> Les Partie-prenantes sont au Produit ce que les acteurs sont au Film : +chacun joue un rôle différent, mais tous mettent en scène la Vision de +ce qu'ils produisent + +## Devenez spectateur un instant + +Votre produit est destiné à être vendu, que ce soit financièrement +auprès de vos clients, ou commercialement auprès de vos utilisateurs. En +général, c'est vous qui présenterez votre Produit, par le biais de +réunions commerciales, de plaquettes publicitaires, ou que sais-je +encore. + + + +Mais avez-vous pensé à laisser vos développeurs faire cette présentation +? Pas de panique, **je ne vous parle pas d'envoyer un geek barbu auprès +de vos prospects et investisseurs**, non. Non, je vous propose de +laisser vos développeurs vous faire une présentation à vous, le +propriétaire du produit. + + + +L'idée est la suivante : on l'a vu, il est plus pratique de travailler +par itérations successives pour votre projet. À la fin de chaque +itération, vos collaborateurs doivent être capables de vous fournir une +version exploitable de votre produit. **Pourquoi dans ce cas ne vous +feraient-ils pas la démonstration de ce qu'ils ont à vous livrer ?** + + + +C'est assez classique dans les préceptes agiles : à la fin de chaque +cycle l'équipe technique va vous présenter ce qu'elle vous propose, et +ce pour différentes raisons : + ++ cela vous permet de valider votre demande initiale ++ cela permet d'impliquer l'équipe technique dans la bonne marche fonctionnelle de votre produit ++ cela permet aux acteurs de votre projet d'éprouver régulièrement de la satisfaction personnelle en réussissant à vous livrer dans les temps quelque chose qui fonctionne + +Une simple présentation d'une heure, pourquoi pas un vendredi sur deux, +par l'équipe technique, vous permet donc d'impliquer dans votre projet +les plus réfractaires : les développeurs. Pourquoi ne pas l'adopter ? + +Bien entendu, encore une fois, ce n'est pas une recette miracle. Mais +l'accumulation des ces instants d'échange et de dialogue ne peut que +faciliter votre projet. + + + +> Laissez l'équipe technique vous faire une démonstration à chaque +cycle. + + +## Tous les acteurs ont des bonnes idées + +Un film, c'est avant tout un réalisateur, qui insuffle l'âme du film, sa +Vision. Pourtant il est très fréquent que de bonnes idées proviennent +d'acteurs : des improvisations, des échanges, des contestations, des +propositions...  + + +Les bonnes idées peuvent venir de partout. Qu'elles soient +fonctionnelles, ergonomiques, esthétiques... vous devez écouter ces +idées, les assimiler et, si certaines vous semblent pertinentes et en +accord avec votre vision, les implémenter. + + +Après tout, votre spécialité c'est la connaissance du domaine +fonctionnel. Votre produit peut répondre à un besoin de bien des +manières, et même d'une façon que vous n'avez pas encore imaginée. + + +Vos développeurs ne connaissent pas à la base votre besoin fonctionnel, +mais leur métier les amène à observer et comprendre une multitude de +manières de répondre à un problème. Chaque site internet qu'ils visitent +répond à une problématique, chaque site a une approche différente des +problèmes, et eux-mêmes sont utilisateurs de produits.  + + +Restez maître de votre Produit, mais, après tout, ce qui compte c'est de +rendre service, au mieux, à vos utilisateurs. Prenez les bonnes idées là +où elles sont... + +> Imprégnez vos développeurs de votre besoin pour leur laisser l'occasion de trouver de bonnes idées diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/func-introduction.md b/data/doc-prince-book-generation/doc/tome1/Contents/func-introduction.md new file mode 100644 index 0000000..c28e6ef --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Contents/func-introduction.md @@ -0,0 +1,56 @@ +# Mettez toutes les chances de votre côté + + +Imaginez : voilà un bon père de famille qui se décide, enfin, a changer +sa vieille voiture. Bien décidé à investir dans du neuf, il se rend chez +le concessionnaire du coin, et explique ce qu'il veut... oh, rien de +bien compliqué : il doit pouvoir aller faire ses courses, partir en +vacances de temps en temps, et, ah si, aller au travail tous les jours. +"Pas de problème, je vous livre votre nouvelle voiture dans deux +semaines" répond le concessionnaire.  + + +Trois semaines plus tard ("ah oui, désolé il y a eu un peu de retard, +mais c'est normal" le rassure le concessionnaire), une petite surprise +attend notre père de famille : ce n'est pas la voiture familiale qu'il +espérait, mais une camionnette, 12m cube, 45 000 € ("on a rencontré des +difficultés imprévues"), 2m10 de haut (la porte de son garage mesure +1m90, mais le concessionnaire est convaincu que "c'est quand même plus +pratique d'avoir une bonne hauteur de coffre. Ah, et je vous ai même +ajouté un volant de course, le top du top !").  + + +Pas très agréable, non ? Ce n'est pas du tout ce que notre père de +famille souhaitait ! Pourtant... Pourtant il peut faire ses courses, +partir en vacances... Tout ce qu'il a demandé au concessionnaire est là. +Le concessionnaire était plein de bonne volonté, il a fait son maximum +pour satisfaire son client, a fait de son mieux, l'a écouté, a tenté d'y +mettre du sien et d'ajouter des options qu'il pense indispensables ou +confortables. On ne va pas l'en blâmer ! + + +Imaginez ça maintenant : **ce père (ou mère) de famille, c'est VOUS** ! +Oui, c'est bel et bien vous, vous qui souhaitez concevoir un produit +informatique, un logiciel, un intranet, une application mobile... Vous +voilà parti avec une superbe idée, rentable, révolutionnaire !  Et vous +vous retrouvez finalement avec un produit coûteux, livré en retard, +voire parfois totalement inadapté au marché ! + + +Allez-vous vous en prendre au développeur ? A la société qui a conçu +votre produit ? Au prestataire de service ? N'est-ce pas plutôt un +problème plus général de communication ? Après tout, chacun est +convaincu d'avoir fait de son mieux... N'y avait-il pas un moyen de +faire mieux ? De mieux faire comprendre votre besoin ? De mieux vous +préparer aux éventuels retard, de les limiter ? De réorienter votre +demande en cours de route ? + + +C'est justement l'objet de ce livre. Bien évidemment, ce livre ne va pas +vous offrir un moyen magique de réussir un projet informatique. Non, ça +n'existe pas, il n'y a jamais de *silver bullet*. Rien n'est magique : +**c'est à vous, et à vous seul, de faire réussir vos projets +informatiques**. Par contre, ce livre peut peut-être vous donner +quelques pistes de réflexion, afin de mettre plus de chances de votre +côté. + diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/func-le-besoin-metier.md b/data/doc-prince-book-generation/doc/tome1/Contents/func-le-besoin-metier.md new file mode 100644 index 0000000..510397b --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Contents/func-le-besoin-metier.md @@ -0,0 +1,309 @@ +# Le besoin métier + +## Ayez une vision de votre produit + +Votre idée est peut-être géniale, révolutionnaire, utile ou juste +rentable (aucun mal à cela). Elle va peut-être changer le monde, ou plus +modestement faciliter le quotidien de pas mal de monde.  + +Pensez au premier téléphone portable : en voilà un produit qui a changé +bien des choses; impossible aujourd'hui d'imaginer vivre sans : sans +accès à votre boîte mail, sans appareil photo, sans sms, sans carnet de +contacts... Mais, attendez, le premier téléphone, il ne faisait pas tout +ça ! Non, rappelez-vous, un téléphone portable, à la base, ça sert à +pouvoir téléphoner n'importe où.  + + +Tout le reste, c'est utile, pratique, génial, sans doute indispensable +au quotidien, bref, c'est tout autant révolutionnaire. Mais si on doit +définir le téléphone portable en quelques mots, c'est "juste" un moyen +d'échanger vocalement et instantanément de l'information entre deux +intervenants, où qu'ils soient dans le monde. + + +Votre produit est pareil : **il doit pouvoir se définir en une courte +phrase**, et en réalité ne sert qu'à une chose. Si si, tout le reste est +utile commercialement, esthétiquement, fonctionnellement... Mais votre +produit possède un coeur, et la première chose à faire est de +l'identifier. + + +Cela vous permettra de savoir vraiment ce dont vous avez besoin. Si vous +demandez au père de famille, dont nous parlions plus haut, de décrire sa +voiture, il va peut-être dire qu'il veut un break Citroen de 120 chevaux +gris métallisé, avec 5 portes. Il sait ce qu'il veut... Pourtant il +oublie l'essentiel : la voiture doit lui permettre de se rendre d'un +point A à un point B. + + +Oui, c'est évident : une voiture permet de se déplacer. Mais pourtant, +évident ou pas, une voiture, ce n'est rien d'autre ; c'est ce qui reste +une fois qu'on a enlevé tout le reste : retirez la climatisation, la +peinture métallisée, le cuir... Il nous reste le coeur de la voiture, ni +plus ni moyen qu'un moyen de locomotion. + + +Allez même plus loin, et imaginez une voiture sans moteur ; est-ce +toujours une voiture ? Oui, sans aucun doute, le moteur n'est qu'un +moyen pour cette fin, ultime, de permettre aux gens de se déplacer : une +voiture à cheval n'en reste pas moins une voiture. Certes, elle n'est +peut-être pas optimale, pas rapide, pas jolie, tout ce que vous +voudrez... Mais C'EST UNE VOITURE. Et peut-être moins cher en plus ! + + +Pour votre logiciel, votre intranet, votre application mobile, c'est +pareil : **identifiez le coeur de votre produit, ce qui fait son essence +même, ce sans quoi votre produit n'est plus votre produit, et là vous +aurez de bonnes bases pour commencer**. + + +Comme le dit Liz Keogh, **chaque projet informatique doit poursuivre une +vision**. Cette vision répond souvent à un besoin économique (réduire +des coûts, améliorer la productivité), voire parfois à des souhaits +différents (améliorer le quotidien des gens, changer les mentalités...). +Dans tous les cas, tout ce que vous allez mettre dans votre produit doit +poursuivre cette vision. + + +> Tout ce que vous mettez dans votre produit doit poursuivre une +vision + +## Le bon produit est celui qui répond à votre vision + +Quand ils ont conçu Basecamp, un logiciel de gestion de projets +aujourd'hui reconnu et plébiscité, Jason Fried et David Heinemer avaient +une vision : faciliter la vie des différents intervenants lors de leurs +créations de projets. Cette vision est visible partout dans Basecamp : +simple, facile d'accès, rapide et ergonomique, il facilite réellement la +vie de celui qui l'utilise. + + +Ils auraient pu choisir de concevoir un logiciel riche, multi-fonction +et polyvalent ; ils ne l'ont pas fait. Pourquoi ? Tout simplement parce +que cela n'aurait pas servi leur vision. On pourrait vouloir un CRM dans +Basecamp, ou un intranet, ou tout ce que vous voudrez. Tous ces modules +pourraient être forts agréables, utiles et d'excellents arguments +commerciaux. Mais ils ne servent pas la vision du produit. Et c'est +justement pour ça que Basecamp plaît autant : il ne fait qu'une chose, +mais il le fait bien ! + + +Pensez également à ceci : certes Basecamp n'est pas riche +fonctionnellement, mais + ++ Il est largement suffisant dans 90% des cas ++ Il a été d'autant moins coûteux à concevoir ++ Il a été d'autant plus rapide à développer ++ Chaque fonctionnalité a pu être testée en profondeur et est fiable ++ Il ne souffre d'aucune complexité inutile et est rapide ++ Les équipes de développement se sont focalisés sur un domaine fonctionnel simple ++ Tout changement ou évolution est simple à mettre en place + + +Tout cela serait impossible si Jason Fried et David Heinemer ne +s'étaient pas focalisés sur leur vision du produit et n'avaient pas +refusé systématiquement d'ajouter des fonctionnalités qui ne répondent +pas à cette vision initiale. + + +Rappelez-vous : vous êtes le seul maître à bord. C'est à vous de vous +servir de votre vision du produit pour limiter le superflu et focaliser +toute l'énergie disponible (développeurs, serveurs, graphistes, +ergonomes...) sur ce qui répond à cette vision, autrement dit sur ce qui +est vraiment utile. + +> Retirez de votre Produit tout ce qui ne sert pas votre Vision + +## Vous attendez des résultats métiers + +Vous avez probablement souvent entendu cette expression : ce qui compte, +ce sont les résultats !  + + +Bon, je ne vais pas vous dire que la fin justifie les moyens, pas du +tout. Non, ce que je veux dire, c'est qu'en investissant du temps et de +l'argent dans votre projet, vous ne devez jamais oublier que ce qui +compte ce sont les résultats métiers (ou fonctionnels) de votre projet. +Ces résultats qui justement servent votre vision. + + +Pensez-y : chaque fonctionnalité, chaque élément d'interface, chaque +bouton, chaque champ de saisie... tout cela doit fournir un bénéfice à +l'utilisateur de votre produit !  + + +Ça peut paraître évident... Mais alors pourquoi consacrer autant +d'énergie sur des choses inutiles fonctionnellement ? Si si, ça arrive +tout le temps ! En tant que développeur j'ai souvent eu l'occasion de le +voir : une grosse partie de mon temps était consacré à du superflu +(gestion des profils utilisateurs hyper poussée, interfaces graphique à +la iGoogle, optimisation prématurée des performances...), alors même que +les fonctionnalités métier n'étaient pas encore finies, voire même pas +spécifiées ! + + +L'énergie dépensée sur un projet doit être corrélée aux bénéfices que +votre produit en tirera pour satisfaire votre vision. Si, alors que les +fonctionnalités métiers ne sont pas pleinement finies, testées, fiables, +éprouvées et ouvertes aux changements, l'énergie dépensée ne sert qu'à +"vendre du rêve", vous risquez la catastrophe.  + + +Concevez votre produit, non pas pour le vendre, mais pour qu'il réponde +à un but (votre vision). Si vous y arrivez, vous aurez à ce moment là en +main les meilleurs atouts pour le vendre. + + +Le bénéfice sera d'autant plus grand que, au lieu de perdre du temps sur +des choses moins utiles, vos développeurs, prestataires ou la société +qui a en charge votre projet, vont mieux comprendre votre besoin, sans se +perdre en détails inutiles pour le moment. + + +En vous focalisant sur les fonctionnalités métiers, vous allez aussi +pouvoir plus rapidement vous confronter au feedback de vos futurs +utilisateurs. Les changements fonctionnels de votre produit seront ainsi +réalisés plus tôt, et donc moins coûteux. + + +> Ne concevez pas votre produit pour le vendre, mais pour qu'il réponde +à un but. Vous aurez alors en main les meilleurs atouts pour le vendre + + + +## Re-priorisez souvent, changez souvent + +Vous l'avez vu : votre produit doit se focaliser sur ce qui apporte à un +métier, sur ce qui répond à votre vision.  + + +C'est bien beau, mais on en est tous conscients : un aspect métier peut +être essentiel aujourd'hui, mais totalement inutile ou différent demain +! Comment gérer ces changements dans votre projet ? + + +Vous vous en doutez, de nombreuses personnes ont tenté de répondre à +cette question. Parmi elles, les (nombreux) auteurs du Manifeste Agile. +Voici certains des principes de ce Manifeste tel qu'ils sont présentés +sur  [http://agilemanifesto.org](agilemanifesto.org), qui nous +intéressent particulièrement : + + + +> "Notre plus haute priorité est de satisfaire le client +> en livrant rapidement et régulièrement des fonctionnalités +> à grande valeur ajoutée. +> +> Accueillez positivement les changements de besoins, +> même tard dans le projet. Les processus Agiles +> exploitent le changement pour donner un avantage +> compétitif au client. +> +> Livrez fréquemment un logiciel opérationnel avec des +> cycles de quelques semaines à quelques mois et une +> préférence pour les plus courts." + + +C'est assez clair : si l'on suit ces principes, largement reconnus +aujourd'hui dans le monde de l'édition logicielle, **le produit doit +être confronté le plus tôt possible au monde réel pour obtenir un +feedback régulier des potentiels utilisateurs**. + + +Ce feedback régulier va vous permettre de réorienter votre produit afin +de supprimer, modifier ou ajouter des comportements dans celui-ci. Vous +avez donc besoin de pouvoir re-prioriser certains éléments, de changer +en cours de route le parcours de développement de votre site, logiciel, +application ou intranet.  + + +En un mot : vous avez besoin de confronter votre produit aux +utilisateurs. Plus vous le confronterez souvent, plus votre produit sera +compétitif, et plus vous aurez besoin de faire évoluer votre produit. + + +Vous l'avez compris : vous avez besoin de changements tout au long de la +phase de conception de votre produit. Cependant, vous l'avez sans doute +déjà vécu, **tout changement dans un produit informatique est +généralement :** + ++ long ++ coûteux ++ source d'erreurs et de bugs ++ difficile et démoralisant pour les équipes + +L'idée pour résoudre ces difficultés est assez simple, mais parfois +difficile à mettre en place, comme nous allons le voir.  + +> Vous devez trouver une solution pour faciliter le changement + +## Gérez votre besoin de changements + +L'idée du Manifeste Agile pour gérer le changement peut paraître simple +parce qu'il "suffit" (entre autres) de modifier les cycles de +développement de votre produit. Généralement, un produit est conçu d'un +seul bloc : on développe un produit non fini pendant une période assez +longue, et on livre un produit fini (enfin, ça c'est qu'on espère !) à +la fin. Cette méthode implique un effet tunnel assez long et souvent +dévastateur.  + + +Qui ne connaît pas le célèbre tableau de Léonard de Vinci : La Joconde ? +Que se serait-il passé si Léonard de Vinci avait dû peindre Mona Lisa +comme on travaille généralement sur un projet informatique ? Cela +donnerait à peu près ceci : + +![ Méthodologie classique : le travail et découpé en lots, chaque lot est totalement réalisé avant de passer à la suite. Le changement fonctionnel en cours de route est difficile. ] +(mona-lisa-incremential.jpg) + +Le travail est tout de suite très fin, et on ne peut le livrer qu'à la +fin... Difficile dans ces conditions d'accepter le changement ! + + +Maintenant examinez la méthode proposée par les fondateurs du Manifeste +Agile. Les équipes techniques vont travailler en cycles, itérativement, +de manière à livrer régulièrement un produit, certes moins complet, mais +exploitable et sur lequel il est possible de fournir un feedback. + + +Le rythme de ces itérations, et leurs objectifs, sont fixés en accord +avec tous (équipes techniques, fonctionnels...), et chacun s'engage à +faire le maximum pour livrer un produit exploitable. Voici ce que cela +donnerait cette fois-ci pour notre cher Léonard : + +![ Méthodologie agile : le tableau s'affine petit à petit. Le changement fonctionnel est facile, même en cours de route] +(mona-lisa-iterative.jpg) + + +Bien sûr, un logiciel qui prendra un an à être développé ne sera pas +exploitable commercialement au bout de trois itérations de deux semaines +! Par contre vous aurez moyen d'obtenir des premiers retours +utilisateurs, et surtout vous allez pouvoir reprioriser et changer +certains éléments en cours de route : la forme du visage ne vous +convient pas ? Faites la changer lors d'une prochaine itération ! Les +couleurs sont trop ternes ? Priorisez un changement des couleurs pour +voir ce qu'en pensent vos futurs utilisateurs. Bref, vous avez compris +l'idée ! + + +Les principes agiles sont simples et évidents, mais ils peuvent être +étrangement contre-intuitifs à mettre en place : souvent, les équipes +(techniques et fonctionnels) ont du mal à se détacher de leurs anciennes +pratiques, quant bien même elles adhèrent aux concepts agiles. + + +J'ai à plusieurs reprises eu l'occasion de voir des entreprises adopter +des méthodologies agiles trop brusquement, ou d'une manière inadaptée. +Les méthodes agiles ne sont pas un remède miracle à tous les maux, il +serait faux de croire que du jour au lendemain des équipes peuvent +oublier toutes leurs anciennes pratiques et que par magie vous serez +livré à temps et avec un produit fini. + + +Mais le changement vers l'agilité peut être simple et rapide, sous deux +conditions :  + ++ **vous DEVEZ vous impliquer dans votre projet !** C'est à vous de prioriser, de dire ce qui va, ne va pas, régulièrement. ++ Vous devez accepter de vous faire aider : **les changements à adopter ne sont pas que méthodologiques, ils sont aussi conceptuels**. De nombreuses sociétés agiles sauront vous guider et vous conseiller. Et croyez moi, l'investissement en vaut la peine, largement ! + +> Itération, feedback; itération, feedback; itération ... \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/func-recette-maintenabilite.md b/data/doc-prince-book-generation/doc/tome1/Contents/func-recette-maintenabilite.md new file mode 100644 index 0000000..862eaaa --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Contents/func-recette-maintenabilite.md @@ -0,0 +1,110 @@ +# Recette et maintenabilité + + +## Chaque recette fonctionnelle peut être automatisée + +La démarche et la syntaxe du Développement piloté a l'avantage d'être +claire et de faciliter la communication. + + +J'ai une excellente nouvelle pour vous : **cette syntaxe va vous +permettre d'automatiser la recette de votre application**. Vraiment ! + + +Et je ne vous parle pas d'outils, si vous en avez déjà utilisé, qui +vont enregistrer puis reproduire les clics et saisies que vous faites +sur une page web (comme avec [Selenium](seleniumhq.org/...)). Non, surtout pas ! +Je vous parle d'outils pour lesquels vous n'avez rien à faire... + + +Après tout, **si vous avez fait l'effort de respecter une syntaxe +spécifique pour décrire vos Scénario, un programme informatique doit +être capable de les interpréter**. + + +Il existe aujourd'hui deux principaux outils sur le marché : Cucumber et +Behat. Le choix de l'outil dépendra du socle technique choisi par vos +développeurs pour votre Projet. + + +Concrètement, ces outils vont transformer vos Scénarios en code +informatique. Ce code informatique sera adapté par vos développeurs qui +lui donneront un sens et s'assureront qu'il signifie bien ce que vous +avez demandé. + + +Par la suite vous disposerez d'une interface où chaque fonctionnalité, +chaque scénario et chaque étape seront, ou bien verte (terminée), ou bien +jaune (pas encore développé), ou bien rouge (ne répond pas au besoin +initial). + + +![ Retrouvez toutes vos fonctionnalités au sein d'une même interface. ](behat-wizard-home.jpg) + +Utiliser de tels outils a un coût puisqu'il exige un travail de +traduction par l'équipe technique. Ce coût peut être important, cela +peut demander du temps non négligeable, en plus d'une montée en +compétence des équipes techniques (très rapide en général), + + +Cependant, en utilisant ces outils : + ++ Les équipes techniques savent si oui ou non elles ont répondu au besoin ++ Les équipes techniques sont rassurées dans leur travail ++ Vous connaissez exactement l'avancée de votre projet ++ Vous pouvez sans risque gérer votre besoin de changement : si une autre fonctionnalité est "cassée" par ce changement, vous le savez tout de suite et pouvez agir en conséquent ++ Vous augmentez le niveau de qualité de votre projet. C'est autant de gains lors de la vie du Produit et de sa maintenance, qui sans cela peut être très coûteuse ++ Vous-même savez en un instant si le livrable est conforme à vos attentes + + +Rien ne vous oblige à utiliser ces composants d'automatisation de +recette. Ne pas les utiliser peut certes être dommage, mais n'enlève +rien à la démarche générale. + + +>Facilitez le changement en automatisant la recette fonctionnel + +## Assurez-vous que les équipes techniques automatisent leurs recettes techniques + +Tester automatiquement qu'un livrable correspond à vos **attentes** +fonctionnelles c'est bien ; mais être certain de la **fiabilité** de +votre produit c'est encore mieux ! + + +La pratique du développement piloté par le comportement, c'est-à-dire de +tout ce que l'on a présenté ici (les fonctionnalités, les scénarios...) +nécessite quelque chose de fondamental : que **vos équipes mobilisent +leur énergie sur votre besoin, pas sur leurs bugs**. + + +Pour cela il n'y a pas énormément de solutions : **vos équipes +techniques doivent créer des tests automatisés de leur code source**.  + + +C'est un impératif fort : c'est le seul moyen pour qu'elles ne vous +disent pas "nous ne pouvons pas développer cette fonctionnalité, c'est +impossible car on risque de casser tout l'existant et d'introduire des +bugs". Non, avec des tests automatisés, une équipe compétente ne peut +pas vous répondre ça. Elle vous dira peut-être que la nouvelle +fonctionnalité a un coût, mais aucune fonctionnalité ne doit être +impossible à implémenter parce qu'elle risque de casser l'existant. + + +Si quelque chose casse lorsque vous introduisez du changement, vous +perdrez beaucoup d'argent : il va falloir identifier (et ça peut être +très long!) et corriger ce qui a cassé, corriger la nouvelle +fonctionnalité, la tester, ajouter des ressources pour le support +applicatif...  + + +Tous ces coûts, très importants sur le long terme, peuvent être réduits +en laissant à court terme une plus grande marge de manœuvre aux équipes +techniques pour fiabiliser leur code source et en écrivant des tests +automatisés. Ne soyez pas un mauvais économe : laissez leur le temps +qu'il faut pour vous fournir un produit plus stable et plus fiable. Vous +y gagnerez sur le long terme. + + +> Laissez le temps aux développeurs de fiabiliser leur code : aucune +fonctionnalité ne doit être impossible à implémenter sous prétexte +qu'elle risque de casser l'existant diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/func-un-peu-de-recul.md b/data/doc-prince-book-generation/doc/tome1/Contents/func-un-peu-de-recul.md new file mode 100644 index 0000000..b83c66a --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Contents/func-un-peu-de-recul.md @@ -0,0 +1,336 @@ +# Un peu de recul sur le Développement piloté par le Comportement + + +## Votre fonctionnalité se spécifie elle-même + +Vous savez désormais décrire votre besoin dans une Langue et une +grammaire compréhensible par tous les acteurs de votre projet. + +Vous avez peut-être déjà rencontré cette difficulté : au fur-et-à-mesure +de la vie d’un projet, la documentation, fruit d’un travail difficile et +chronophage, se détache généralement de la réalité. + +Cette difficulté a en réalité une origine simple : lorsque l’on ajoute +une nouvelle fonctionnalité, mettre à jour la documentation prend +parfois plus de  temps que la réalisation même de la fonctionnalité. + +Maintenant, examinez ceci : avec le Développement piloté par le +Comportement, c’est-à-dire grâce à la Langue Commune, grâce une +grammaire spécifique, la documentation fonctionnelle est écrite en même +temps que les spécifications. Et oui! Vos Fonctionnalités sont aussi de +la documentation. + +Il est assez facile d’imaginer, par exemple, prendre l’ensemble de vos +fichiers de Fonctionnalités et les transformer en un fichier .pdf, ou +encore d’en faire un manuel en ligne. C'est ce que font, par exemple, +[Relish](https://www.relishapp.com) (hébergé, payant) ou +[Pickles](https://github.com/picklesdoc/pickles) (gratuit, à installer soi-même). + +Bien plus, aucun changement fonctionnel de code source ne peut être +réalisé sans que l’un de vos fichiers de Fonctionnalités  n’ait été +modifié. C’est le principe même du Développement piloté par le +Comportement : vous ne pouvez pas avoir de delta entre votre expression +de besoin et ce que vos développeurs vous livreront. + +Par conséquent, vous l’aurez compris, vous avez ainsi un énorme avantage +:  **les Fonctionnalités que vous avez constituées  reflètent en +permanence l’état réel de votre Produit**.  + + +Si ce n'est pas le cas, c'est : + ++ que vous avez modifié votre Fonctionnalité sans en rédiger les Scénarios directement avec l'équipe technique. Votre erreur sera d'avoir rompu le dialogue avec vos équipes, ++ ou que les équipes n'auront pas encore réalisé votre demande de changement. Il faudra dans ce cas prioriser ce changement parmi l'ensemble de fonctionnalités qu'ils ont à développer. + +> Spécification = Documentation = toujours à jour + +## Servez-vous de vos fonctionnalités pour prioriser + +Pensez à ceci : vous pouvez, si vous le souhaitez, associer un état à +chacun de vos fonctionnalités et scénarios. Par exemple : + ++ En attente ++ En cours de développement ++ Réalisé + + +Vous verrez qu'il existe des outils pour vous aider dans cette tâche. +Grâce à ces indicateurs, vous disposez de la possibilité de visualiser à +chaque instant l'état de votre produit. + + +Si, donc, vous connaissez la phase de développement de chacun de ces +scénarios et fonctionnalités, pourquoi ne pas vous servir de cette +information pour changer leur priorité ?  + + +Vous le savez, votre Produit a besoin d'être confronté rapidement au +feedback de vos utilisateurs. Profitez en pour faire en sorte de vous +faire livrer régulièrement des versions simplifiées de votre projet ; +regardez ce qui plaît à vos utilisateurs... C'est justement ce qui plaît +qu'il va falloir développer en priorité dans votre projet, tout le reste +peut être remis à plus tard. + + +Vous l'aurez compris : ce découpage en fonctionnalités, puis en +scénarios, va vous permettre de prioriser intelligemment votre projet. +Bien plus, cela va vous pousser à abandonner certains points pour en +introduire de nouveaux, plus pertinents pour vos utilisateurs finaux ; +et puisque cela fait partie de votre Produit même, ce changement +fonctionnel sera à la fois rapide et peu coûteux ! + + +> Vous avez un aperçu en temps réel de l'état de votre Produit + + +## Mesurez le chemin parcouru, pas l'énergie dépensée + +C'est souvent la même chose : on surveille le temps passé, les tâches +réalisées... Mais on a tous le même problème : passer beaucoup de temps +ne veut pas dire obtenir un résultat. **Pourquoi mesurer ce temps passé +si vous n'avez pas le résultat attendu** **?** Ce qui compte, ce sont +uniquement les fonctionnalités. + + +Les outils classiques vous proposent de découper chaque besoin en tâche, +puis chaque tâche en temps. Or une tâche n'a aucun sens fonctionnel ! +Vraiment ! C'est comme si vous demandiez à un chauffeur routier combien +de litres d'essence il a consommé ; certes, c'est important d'un point de +vue financier, mais ce qui compte c'est quand même de savoir s'il a pu +livrer tous ses clients, vous ne croyez pas ? + + +Dans un projet informatique c'est pareil : focalisez-vous sur les +fonctionnalités développées. Après tout, c'est ce qui compte vraiment... +Uniquement ! + + +Bien entendu, peut-être que les équipes techniques, elles, travailleront +en tâches, mais : + ++ rien ne les y oblige. ++ ça ne vous concerne pas, seules les fonctionnalités vous intéressent + + +**On n'a jamais vu un client obtenir un bénéfice métier parce qu'une +équipe a accompli une tâche**. Non, le bénéfice ne s'obtient qu'en +fournissant un service, autrement dit une fonctionnalité. + + +> Le client final n'est pas content parce qu'une tâche est accomplie ; +il est content parce qu'une fonctionnalité lui est offerte et qu'elle +répond a son besoin + +## Une bonne fonctionnalité sert la Vision Produit + +Prenez le réflexe : demandez-vous systématiquement si votre +fonctionnalité sert votre Vision. Si ce n'est pas le cas ... Et bien +oubliez la ! + + +A contrario, ne vous limitez pas à un cadre. Après tout, ce qui compte vraiment ce +n'est pas la somme de fonctionnalités, c'est de réussir à satisfaire +votre Vision. + + +Prenez GMail, de Google, par exemple. Chaque fonctionnalité est +sélectionnée pour répondre à cette vision simple : "permettre à chacun +de communiquer facilement avec ses proches". Peu importe le cadre ; du +moment qu'une fonctionnalité sert cette vision, elle est bonne. + + +Sans Vision, GMail ne serait qu'une application, c'est à dire un simple +webmail. Mais c'est  justement pour cela que GMail c'est : + ++ un webmail ++ un service de communication instantané (GTalk)  ++ une application mobile ++ un tchat vidéo (hangout) ++ un rappel des anniversaires de vos amis ++ un carnet de contacts ++ ... + + +Tous ces services desservent la même vision. C'est ce qui fait que GMail +plaît autant. Et c'est cette vision qui assure la cohérence de +l'ensemble : **il vous faudra parfois refuser certaines +fonctionnalités**, même si à court terme elles semblent rentables.  + + +Si elles ne répondent pas à la même vision, ces fonctionnalités vont +rendre votre produit incohérent et illogique. Pourquoi dépenser de +l'argent pour rendre votre Produit illogique ? **Tout le monde aime les +choses simples, claires et cohérentes** ; pire : si vous enlevez cette +clarté, vos développeurs eux-mêmes risquent de ne plus comprendre ce que +vous voulez ! + + +Si vous tenez absolument à ajouter une fonctionnalité qui sert une autre +vision, créez un autre projet, avec d'autres équipes, mais ne la +greffez pas artificiellement à celui-ci. + + +> Respecter scrupuleusement une Vision assure la cohérence et la clarté +du Produit + + + +## Oubliez l'interface graphique + +Fatigué ? Pourquoi ne pas faire un petit jeu ? Vous connaissez Google +Calendar ? mais si, vous savez, le calendrier de Google, très pratique, +que vous retrouvez sur calendar.google.com... + + +A votre avis : + + +| Google Calendar serait-il toujours le même produit : | | +| --------------------------------------------------------- | ----------------- | +| Si le site web n'affichait plus la date du jour ? | ☐ oui ☐ non | +| Sans page web pour y accéder ? | ☐ oui ☐ non | +| S'il n'affichait pas de calendrier mensuel | ☐ oui ☐ non | +| S'il ne vous permettait pas d'ajouter un rendez-vous en cliquant sur le bouton "nouveau rendez-vous" ?| ☐ oui ☐ non | +| Si Internet n’existait pas ? | ☐ oui ☐ non | + + +Au risque de surprendre, la réponse est partout la même : "oui" ! Oui, +même si Internet n'existait pas ! + + +Après tout, qui nous dit que le web que nous connaissons ne va pas +disparaître ? Google Calendar pourrait très bien n'exister que sur +téléphone mobile. Mieux, sans électronique. Après tout, si Google avait +existé au début du XXème siècle, il lui aurait fallu trouver un autre +moyen pour délivrer ses services. + + +Considérez en effet cette Vision de Google Calendar : "permettre à +chacun de connaître facilement  les événements à venir qui le +concernent". Pas besoin de page web, d'application mobile... Un simple +courrier postal pour ajouter un événement, puis quelqu'un qui vient frapper à +notre porte à chaque fois qu'un événement va arriver... C'est suffisant, non ? + + +Certes, commercialement ce n'est pas l'idéal, mais même si Google +Calendar changeait pour désormais envoyer quelqu'un frapper chez nous à +chaque événement, le Produit serait inchangé ; seul le support matériel +serait modifié. Autrement dit : **le support n'a aucune importance +lorsque vous décrivez votre Produit !** + + +Quand vous décrivez votre produit, écrivez vos fonctionnalités de sorte +à ce qu'elles soient valables, même sur internet, même sur application +mobile, et même par courrier postal. C'est la seule façon de pérenniser +votre projet. Si vous avez un site web, vos Fonctionnalités et Scénarios +ne doivent pas changer d'un pouce lorsque vous décrivez votre +application mobile. + + +> Focalisez-vous sur le Comportement de votre Produit, et laissez +l'interface graphique aux ergonomes et aux graphistes. + + +## Le scénario n'est pas un critère d'acceptation + +Considérez ce scénario, mis en évidence par Liz Keogh: + + [gherkin] + Scénario : pouvoir acheter un animal domestique adulte + Etant donné qu'un chiot est trop petit pour être vendu + Quand j'essaye de l'acheter + Alors je suis informé qu'il ne peut être vendu car trop jeune + +Bien. Bien ? Non, pas vraiment... Vous n'avez pas décrit votre besoin, +vous venez en réalité de fournir un Critère d'acceptation générique à +vos développeurs. Leur réaction face à ce scénario ne peut être que +"d'accord, mais à quel âge par exemple le chiot ne peut pas être vendu +?". + + +Tant qu'il reste des questions en suspend, quelque chose ne va pas. +N'oubliez pas que **vous décrivez vos Fonctionnalités et vos Scénarios, non +pas d'abord pour valider un travail fourni, mais avant tout pour être +sûr que tout le monde a bien compris votre besoin** et pour éviter les +allers et retours inutiles. + + +Chaque scénario doit contenir ses propres exemples. Notre scénario +pourrait être changé en : + + [gherkin] + Scénario : pouvoir acheter un animal domestique adulte + Etant donné qu'un chiot ne peut être vendu avant qu'il n'ait "2 mois" + Et que "Médor le chien" a actuellement "1 mois" + Quand j'essaye d'acheter "Médor le chien" + Alors on doit me dire "Médor le chient est trop jeune. Veuillez revenir dans 1 mois" + +C’est un peu plus explicite non ? Et il reste moins de questions. De +cette manière vous limitez les aller-et-venues inutiles et exprimez +votre besoin clairement. + + +> Si on vous demande: "Pouvez-vous me donner un exemple où cette +situation arrive?", vous n'avez pas rédigé un Scénario mais un Critère +d'acceptation. Effacez le. + + +## Les principales causes d'échec du BDD + +Je vais être franc : cette voie de communication, le Développement +Piloté par le Comportement, n'est pas une recette miracle et peut +échouer. + + +Vous pouvez, comme pour tout, y perdre de l'argent si vous échouez à +appliquer ce principe fondamental : **ce qui compte c'est de réussir à +ce que chacun comprenne clairement votre besoin vis-à-vis de votre +Vision**. + + +Je pense que la première cause d'échec vient de la **difficulté à +identifier clairement les fonctionnalités d'un Produit**. On a tous en +tête des idées géniales, le site web qui va tuer Facebook et eBay ; le +vrai souci est de savoir comment, dans la pratique, ce site web va rendre +service aux utilisateurs, et quelles sont les fonctionnalités qu'on va lui +offrir. + + +La seconde difficulté réside dans notre propension à avoir besoin d'un +support visuel pour réfléchir à quelque chose. **Il n'est pas simple de +ne se focaliser uniquement sur le Comportement d'un Produit, sans se +soucier de son apparence**, de son interface graphique. Ce sera d'autant +plus vrai si vous avez un profil technique ou si vous êtes très +familiarisé avec les nouvelles technologies.  + + +Pour résoudre cette difficulté, tentez systématiquement de vous +représenter votre produit sur différents supports : un ordinateur, un +téléphone, mais aussi par courrier ou par pigeon voyageur ! Bref, faites +abstraction totale du support matériel. + + +Ne vous focalisez pas sur le cheminement qui permet à votre application +ou site web de délivrer un service. Partez en sens inverse depuis +l'utilisateur : quel comportement peut-il avoir avec mon Produit ? +Comment mon Produit peut-il l'aider ? + + +Enfin, et c'est le plus important : **vous aurez besoin de vous faire +conseiller, surtout si ce n'est pas votre premier projet informatique**. +Il est quasiment impossible de réussir du premier coup à se défaire des +habitudes antérieures. + + +Il existe aujourd'hui de nombreuses sociétés de conseil en Agilité qui +sauront vous conseiller, surtout dans la démarche de dialogue qu'il vous +faudra engager avec vos équipes. L'investissement risque fort d'en +valoir la peine... Si vous n'en avez pas les moyens vous pouvez (très +sérieusement) effectuer en parallèle ce dialogue auprès de personnes +totalement novices dans le domaine fonctionnel abordé et peu familières +des nouvelles technologies : vos parents, grand-parents, enfants ou vos +amis ermites. + + +> Il n'existe pas de recette miracle. Faites abstraction de tout +support, oubliez vos habitudes et demandez de l'aide diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/behat-wizard-edit.jpg b/data/doc-prince-book-generation/doc/tome1/Contents/images/behat-wizard-edit.jpg new file mode 100644 index 0000000..d72484b Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/behat-wizard-edit.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/behat-wizard-home.jpg b/data/doc-prince-book-generation/doc/tome1/Contents/images/behat-wizard-home.jpg new file mode 100644 index 0000000..2757982 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/behat-wizard-home.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/cartographie-or.png b/data/doc-prince-book-generation/doc/tome1/Contents/images/cartographie-or.png new file mode 100644 index 0000000..583b0c1 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/cartographie-or.png differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/cartographie.png b/data/doc-prince-book-generation/doc/tome1/Contents/images/cartographie.png new file mode 100644 index 0000000..61bb666 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/cartographie.png differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/cover.jpg b/data/doc-prince-book-generation/doc/tome1/Contents/images/cover.jpg new file mode 100644 index 0000000..a908ead Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/cover.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/cover.old.jpg b/data/doc-prince-book-generation/doc/tome1/Contents/images/cover.old.jpg new file mode 100644 index 0000000..a908ead Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/cover.old.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-behat-exo-date-fail.jpg b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-behat-exo-date-fail.jpg new file mode 100644 index 0000000..ee8fa5d Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-behat-exo-date-fail.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-behat-exo-date-undefined.jpg b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-behat-exo-date-undefined.jpg new file mode 100644 index 0000000..a2b86e2 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-behat-exo-date-undefined.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo1-step1.png b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo1-step1.png new file mode 100644 index 0000000..8668632 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo1-step1.png differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo1-step2.png b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo1-step2.png new file mode 100644 index 0000000..13d1685 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo1-step2.png differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo2.png b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo2.png new file mode 100644 index 0000000..00933c4 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo2.png differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo3.png b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo3.png new file mode 100644 index 0000000..3f5c12f Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/dev-qr-exo3.png differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/mona-lisa-incremential.jpg b/data/doc-prince-book-generation/doc/tome1/Contents/images/mona-lisa-incremential.jpg new file mode 100644 index 0000000..01cfcfd Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/mona-lisa-incremential.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/mona-lisa-iterative.jpg b/data/doc-prince-book-generation/doc/tome1/Contents/images/mona-lisa-iterative.jpg new file mode 100644 index 0000000..e47a402 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/mona-lisa-iterative.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/phpunit.png b/data/doc-prince-book-generation/doc/tome1/Contents/images/phpunit.png new file mode 100644 index 0000000..8661767 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/phpunit.png differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/images/screen-export-html.jpg b/data/doc-prince-book-generation/doc/tome1/Contents/images/screen-export-html.jpg new file mode 100644 index 0000000..22c9a47 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Contents/images/screen-export-html.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Contents/license.md b/data/doc-prince-book-generation/doc/tome1/Contents/license.md new file mode 100644 index 0000000..7038908 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Contents/license.md @@ -0,0 +1,8 @@ +Ce livre est publié sous licence Creative Commons. + +Vous pouvez contribuer à son amélioration sur [Github](http://wwww.github.com/Halleck45/book-bdd) + +Copyright (c) Jean-François Lépine + +Ce livre de [Jean-François Lépine](http://communiquez.lepine.pro) est mis à disposition selon les termes de la [licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé](http://creativecommons.org/licenses/by-nc-sa/3.0/deed.fr). +Fondé(e) sur une œuvre à [https://github.com/Halleck45/livre-developpement-pilote-comportement](https://github.com/Halleck45/livre-developpement-pilote-comportement). \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/tome1/Resources/Templates/cover-small.jpg b/data/doc-prince-book-generation/doc/tome1/Resources/Templates/cover-small.jpg new file mode 100644 index 0000000..988a180 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Resources/Templates/cover-small.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Resources/Templates/cover.jpg b/data/doc-prince-book-generation/doc/tome1/Resources/Templates/cover.jpg new file mode 100644 index 0000000..7ac1451 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Resources/Templates/cover.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome1/Resources/Templates/cover.pdf b/data/doc-prince-book-generation/doc/tome1/Resources/Templates/cover.pdf new file mode 100644 index 0000000..df97ec9 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome1/Resources/Templates/cover.pdf differ diff --git a/data/doc-prince-book-generation/doc/tome1/Resources/Templates/print/style.css b/data/doc-prince-book-generation/doc/tome1/Resources/Templates/print/style.css new file mode 100644 index 0000000..53f5b06 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Resources/Templates/print/style.css @@ -0,0 +1,83 @@ +body { + color: #333333; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + line-height: 20px; +} +p strong, li strong { + color:#008287; +} +.code { + background:#000; +} +.code { + color:#999; +} + +h1 { + +} +h2 { + background-color: #F7F7F9; + border: 1px solid #E1E1E8; + padding:10px; + margin-top:30px; +} +h3 { + font-size:14px !important; +} + +blockquote p { + font-size:18px; + background:#CCC; + color:#333; + width:80%; + font-style: italic; + margin-left:15%; + padding:10px; +} + +.item.toc .part.level-1 { + color:#F7F7F9; + background-color: #000; + border: 1px solid #E1E1E8; + padding:10px; + margin-top:30px; + font-size:26px; +} +.item.part h1 { + text-align:center; + font-size:70px; +} + + +/* --- gherkin --- */ +/*texte*/ +div.code.gherkin { + color:#C5C8C6; +} +/*commentaire*/ +div.code.gherkin .co1 { + color:#969896; +} +/*clef*/ +div.code.gherkin .co3 { + color:#DE935F; +} +/*Exemples*/ +div.code.gherkin .co4 { + color:#DE935F; +} +/*mot clef*/ +div.code.gherkin .kw1 { + color:#81A2BE; +} +/* +vert #B5BD68 +orange #DE935F +violet #B294BB +bleu #81A2BE +blanc #C5C8C6 +gris #969896 +jaune #B5BD68 +*/ \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/tome1/Resources/Templates/web/style.css b/data/doc-prince-book-generation/doc/tome1/Resources/Templates/web/style.css new file mode 100644 index 0000000..f4f144a --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/Resources/Templates/web/style.css @@ -0,0 +1,63 @@ +body { + color: #333333; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + line-height: 20px; +} + +.code { + background:#000; +} +.code { + color:#999; +} + +h2 { + background-color: #F7F7F9; + border: 1px solid #E1E1E8; + padding:10px; + margin-top:30px; +} +h3 { + font-size:14px !important; +} + +em { + font-size:12px; + color:#c20cb9; +} +.item.part h1 { + text-align:center; + font-size:40px; +} + +/* --- gherkin --- */ +/*texte*/ +div.code.gherkin { + color:#C5C8C6; +} +/*commentaire*/ +div.code.gherkin .co1 { + color:#969896; +} +/*clef*/ +div.code.gherkin .co3 { + color:#DE935F; +} +/*Exemples*/ +div.code.gherkin .co4 { + color:#DE935F; +} +/*mot clef*/ +div.code.gherkin .kw1 { + color:#81A2BE; +} +/* +vert #B5BD68 +orange #DE935F +violet #B294BB +bleu #81A2BE +blanc #C5C8C6 +gris #969896 +jaune #B5BD68 +*/ \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/tome1/config.yml b/data/doc-prince-book-generation/doc/tome1/config.yml new file mode 100644 index 0000000..7f70104 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome1/config.yml @@ -0,0 +1,74 @@ +book: + title: "Le développement piloté par le comportement" + subtitle: "Partie 1 : Communiquez avec les développeurs" + author: "Jean-François Lépine" + edition: "First edition" + language: fr + publication_date: ~ + + generator: { name: easybook, version: 5.0-DEV } + + contents: + # available content types: acknowledgement, afterword, appendix, author, + # chapter, conclusion, cover, dedication, edition, epilogue, foreword, + # glossary, introduction, license, lof (list of figures), lot (list of + # tables), part, preface, prologue, title, toc (table of contents) + - { element: cover } + - { element: toc } + + - { element: introduction, content: func-introduction.md } + - { element: chapter, number: 1, content: func-le-besoin-metier.md } + - { element: chapter, number: 2, content: func-communiquez.md } + - { element: chapter, number: 3, content: func-impliquez-les-parties-prenantes.md } + - { element: chapter, number: 4, content: func-exprimez-le-besoin.md } + - { element: chapter, number: 5, content: func-un-peu-de-recul.md } + - { element: chapter, number: 6, content: func-recette-maintenabilite.md } + - { element: chapter, number: 7, content: func-conclusion.md } + + - { element: author, content: author.md } + - { element: license, content: license.md } + + + + editions: + ebook: + format: epub + highlight_code: false + include_styles: true + labels: ['appendix', 'chapter'] # labels also available for: "figure", "table" + theme: clean + toc: + deep: 1 + elements: ["appendix", "chapter", "part"] + + print: + format: pdf + highlight_code: true + include_styles: true + isbn: ~ + labels: ["appendix", "chapter"] # labels also available for: "figure", "table" + margin: + top: 25mm + bottom: 25mm + inner: 30mm + outter: 20mm + page_size: A4 + theme: clean + toc: + deep: 2 + elements: ["appendix", "chapter", "part"] + two_sided: true + + web: + format: html + highlight_code: true + include_styles: true + labels: ["appendix", "chapter"] # labels also available for: "figure", "table" + theme: clean + toc: + deep: 2 + elements: ["appendix", "chapter", "part"] + + website: + extends: web + format: html_chunked diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/author.md b/data/doc-prince-book-generation/doc/tome2/Contents/author.md new file mode 100644 index 0000000..a771593 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Contents/author.md @@ -0,0 +1,7 @@ +Je m'appelle Jean-François Lépine, et je suis consultant Web. Au quotidien, j'assiste les entreprises dans la conception et la réalisation de leurs applications web. + +Très impliqué dans l'éco-système OpenSource Web, j'anime le blog technique blog.lepine.pro. Vous pouvez retrouver quelques-unes de mes contributions sur mon [compte Github](http://www.github.com/Halleck45). + +Je suis également l'auteur du Mémento Industrialisation PHP : outils et qualité publié en 2012 aux éditions Eyrolles. + +Ces livres sont gratuits et open source. S'ils vous ont plu, n'hésitez pas à me le signaler sur [twitter](https://www.twitter.com/Halleck45) :-) diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/dev-automatisez-votre-recette.md b/data/doc-prince-book-generation/doc/tome2/Contents/dev-automatisez-votre-recette.md new file mode 100644 index 0000000..b8aaa5b --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Contents/dev-automatisez-votre-recette.md @@ -0,0 +1,760 @@ +# Automatisez votre recette + +Vous l'avez vu, Gherkin est un moyen simple pour votre fonctionnel pour vous fournir +une expression de besoin claire et précise. + +Que diriez-vous maintenant de traduire ce besoin en tests automatisés ? Car, oui, c'est possible ! + +Tout besoin, s'il est exprimé clairement, peut être testé. Si ce besoin est exprimé dans une +grammaire solide et complète, tout système d'information peut le tester automatiquement. + +Je vous propose de voir comment traduire ce besoin en tests automatisés, de telle sorte +qu'une simple ligne de commande dans un terminal vous indique si ce que vous avez développé est +conforme ou non. + +L'ensemble des exemples que nous allons voir est écrit en PHP. Pourquoi ? Tout simplement +parce que la communauté PHP a très fortement et très rapidement adhéré au Développement +piloté par le comportement. Les outils PHP pour le Développement piloté par +le comportement sont matures, nombreux, et surtout offrent une souplesse qui, à ce jour, +ne se retrouve pas dans d'autres langages. + +Cependant, il existe des outils pour écrire et lancer des tests automatisés dans à +peu près n'importe quel langage : + ++ Ruby : Cucumber (http://cukes.info) ++ Java : JBehave (http://jbehave.org) ++ C# : NBehave (https://github.com/nbehave/NBehave) ++ Python : Behave (http://packages.python.org/behave/) ++ PHP : Behat (http://behat.org) , PHPSpec (http://www.phpspec.net) ++ JavaScript : Jasmine (http://pivotal.github.com/jasmine/) ++ Net : NBehave (http://nbehave.org) + +Il en existe bien d'autres, la liste est loin d'être exhaustive, mais ceux-ci sont particulièrement +utilisés dans le monde du développement aujourd'hui. + + + + +## Installez et utilisez Behat en PHP + +Behat est un outil développé en PHP par Konstantin Kudryashov (@everzet) et soutenu +par la société KnpLabs. Cet outil efficace est aujourd'hui mature et largement utilisé. +Behat permet d'exécuter des tests automatisés sur tous types d'applications, PHP ou non. +Il peut tester : + ++ des applications web, en pilotant un vrai navigateur ou non ++ des applications console (terminal) ++ des morceaux de code source + +Du moment que ce qui est testé dispose d'un point d'entrée et d'une information de sortie, +Behat va vous permettre d'automatiser votre recette métier. + +Dans tous les cas, il vous faudra PHP installé sur la machine qui exécutera les tests. +L'installation de PHP est résumée sur cette page : http://php.net/manual/fr/install.php. + +Windows : + + [bash] + Utilisez WampServer (http://www.wampserver.com) + +Ubuntu, Debian : + + [bash] + # en ligne de commande + apt-get install php5-common php5-cli php5-curl + +Mac : + + [bash] + Utilisez MampServer (http://www.mamp.info) + + +Installer Behat est simple, et peut être fait de plusieurs manières : sous forme d'archive PHP +(phar) ou en utilisant un gestionnaire de dépendance. + +### Installation avec Composer + +Créez un fichier composer.json à la racine de votre projet, avec le contenu suivant : + + [json] + { + "require-dev": { + "behat/behat": "2.5.*@stable" + }, + "config": { + "bin-dir": "bin/" + } + } + + +Exécutez ensuite la commande suivante dans votre terminal, en vous plaçant à la racine +de votre projet : + + [bash] + curl http://getcomposer.org/installer | php + php composer.phar install --prefer-source + +### Installation sous forme d'archive Phar + +Il vous suffit de télécharger le fichier behat.phar à l'adresse suivante : http://behat.org/ +downloads/behat.phar. Placez-le ensuite dans un dossier `bin` à la racine de votre projet. Vous +aurez donc l'arborescence suivante : + + [bash] + /chemin/vers/mon/projet + (...) + - bin/ + - behat.phar + + +Pour des raisons de maintenabilité, il est préférable d'utiliser la méthode d'installation +avec Composer, qui vous permettra de mettre à jour très facilement l'ensemble des librairies +PHP que vous utilisez, y compris Behat. + +### Préparer le projet + +Une fois que Behat est installé, il vous suffit d'exécuter la commande suivante : + + [bash] + php ./bin/behat --init + +Cela aura pour effet de créer dans votre projet tous les éléments dont Behat a besoin pour +fonctionner. + +Vous voici désormais avec les dossiers suivants : + ++ `features` : contient la listes des fonctionnalités, sous forme de fichier `.feature` ++ `features/bootstrap` : contient les fichiers PHP de traduction de Fonctionnalité en code source + +Chaque nouvelle fonctionnalité devra donc être ajoutée dans le dossier `features`, sous forme d'un +fichier `.feature`. Créez par exemple le fichier `features/calculer-mon-age.feature`, avec le +contenu suivant : + + +![ Les sources de cet exercice sont disponibles sur http://goo.gl/lgWvy ] +(dev-qr-exo1-step1.png) + + + [gherkin] + # language: fr + Fonctionnalité: Calculer l'âge d'une personne + En tant qu'utilisateur de l'application + Je veux connaître le nombre d'années écoulées entre deux dates + De telle sorte que je puisse connaître mon age + + Scénario: Calculer l'âge d'une personne depuis une date antérieure à aujourd'hui + Etant donné que je suis né le 06/07/1986 + Et que nous sommes le 20/09/2013 + Quand je calcule mon âge + Alors je suis informé que j'ai 27 ans + + Scénario: Calculer l'âge d'une personne depuis une date postérieure à aujourd'hui + Etant donné que je suis né le 06/07/3013 + Et que nous sommes le 20/09/2013 + Quand je calcule mon âge + Alors je suis informé que je ne suis pas encore né + + Scénario: Calculer l'âge d'une personne dont c'est l'anniversaire aujourd'hui + Etant donné que je suis né le 06/07/1986 + Et que nous sommes le 06/07/2013 + Quand je calcule mon âge + Alors je suis informé que j'ai 27 ans + Et on me souhaite un joyeux anniversaire + +Puis exécutez la commande suivante pour lancer Behat : + + [bash] + ./bin/behat + +![ Les étapes de la fonctionnalité ne sont pas encore traduites : elles sont jaunes ] +(dev-behat-exo-date-undefined.jpg) + +Comme vous pouvez le voir, le résultat de cette commande est jaune. Cela veut tout simplement dire +que les phrases utilisées pour exprimer le besoin n'ont pas encore de signification pour Behat. Il va falloir +les traduire. + + +## Traduire une Fonctionnalité en code source + + +Par chance, Behat est suffisamment bien fait pour vous fournir une base de travail pour traduire vos fonctionnalités. Il suffit +de copier-coller dans le fichier `features/bootstrap/FeatureContext.php` le code PHP qui a été généré par la commande : + + [bash] + ./bin/behat + +Pourquoi copier ce code dans ce fichier ? Tout simplement parce que les fichiers `features/bootstrap/*Context.php` vont servir +de passerelle entre le besoin exprimé (sous forme de phrases) et votre application. + +Il vous appartient de modifier ces fichiers pour faire la traduction du besoin fonctionnel. C'est un travail +qui semble long, mais en réalité il n'est pas beaucoup plus long que de tester vous-même à main que la demande initiale +est respectée, mais possède surtout l'avantage de rendre ce travail de recette interne totalement automatique. + +Traduisez maintenant la fonctionnalité en code source dans le fichier `features/bootstrap/FeatureContext.php`, en adaptant +le code PHP fourni par Behat selon votre besoin. Par exemple : + + [php] + birthDate = new \DateTime(sprintf('%d-%d-%d', $year, $month, $day)); + } + + /** + * @Given /^que nous sommes le (\d+)\/(\d+)\/(\d+)$/ + */ + public function queNousSommesLe($day, $month, $year) + { + $this->today = new \DateTime(sprintf('%d-%d-%d', $year, $month, $day)); + } + + /** + * @When /^je calcule mon âge$/ + */ + public function jeCalculeMonAge() + { + $this->output = shell_exec(sprintf('php src/age.php --birthdate=%s --today=%s', $this->birthDate->format('Y-m-d'), $this->today->format('Y-m-d'))); + } + + /** + * @Then /^je suis informé que j\'ai (\d+) ans$/ + */ + public function jeSuisInformeQueJAiAns($age) + { + if(!preg_match('!'.$age.' ans!', $this->output)) { + throw new Exception(); + } + } + + /** + * @Then /^je suis informé que je ne suis pas encore né$/ + */ + public function jeSuisInformeQueJeNeSuisPasEncoreNe() + { + if(!preg_match('!Vous n\'êtes pas encore né!', $this->output)) { + throw new Exception(); + } + } + + /** + * @Then /^on me souhaite un joyeux anniversaire$/ + */ + public function onMeSouhaiteUnJoyeuxAnniversaire() + { + if(!preg_match('!Joyeux anniversaire!', $this->output)) { + throw new Exception(); + } + } + } + +Notez ces différents aspects : + ++ chaque phrase est convertie en une méthode PHP ++ le lien entre les phrases et les méthodes PHP est effectué par un expression régulière (par exemple : `/^on me souhaite un joyeux anniversaire$/`) ++ vous devez traduire chaque étape. Behat comprendra qu'une étape n'est pas valide si vous levez une exception, comme c'est le cas dans la méthode `onMeSouhaiteUnJoyeuxAnniversaire()` ++ il est possible de recevoir certaines informations (nombres, exemples...) sous forme de paramètres de méthodes. ++ l'application qui va être testée fonctionne en ligne de commande (`php src/age.php`) + +Maintenant, il vous suffit de relancer Behat pour vérifier automatiquement que votre application a bien +le comportement attendu : + + [bash] + ./bin/behat + +![ Les tests échouent ; c'est normal, les fonctionnalités n'ont pas encore été développées ] +(dev-behat-exo-date-fail.jpg) + + +Il vous suffit finalement de développer votre application PHP, conforme aux attentes fonctionnelles, puis de relancer +Behat pour vous assurer que vous avez bien traité la demande initiale. + + +## Exploitez les jeux d'exemples + +![ Les sources de cet exercice sont disponibles sur http://goo.gl/jaLxP ] +(dev-qr-exo1-step2.png) + +Vous l'avez vu, votre fonctionnel peut vous fournir des exemples, grâce à la syntaxe Gherkin. + +On pourrait par exemple modifier notre fonctionnalité de la façon suivante : + + [gherkin] + # language: fr + Fonctionnalité: Calculer l'âge d'une personne + En tant qu'utilisateur de l'application + Je veux connaître le nombre d'années écoulées entre deux dates + De telle sorte que je puisse connaître mon age + + Plan du Scénario: Calculer l'âge d'une personne + Etant donné que je suis né le "" + Et que nous sommes le "" + Quand je calcule mon âge + Alors on me répond "" + + Exemples: + | dateNaissance | dateDuJour | reponseAttendue | + | 06/07/1986 | 20/09/2013 | Vous avez 27 ans | + | 06/07/1985 | 20/09/2013 | Vous avez 28 ans | + | 26/11/2020 | 20/09/2013 | Vous n'êtes pas encore né | + | 06/07/1986 | 06/07/2013 | Vous avez 27 ans. Joyeux anniversaire | + + +Vous voici avec une fonctionnalité simple, claire et complète. + +Travailler avec des exemples ne change strictement rien pour vous. En réalité, Behat +va travailler avec chaque jeu d'exemple, un à un, et va vous envoyer chaque information +sous forme de paramètre de méthode, sans que cela n'ait le moindre impact sur votre code. + +Voici une traduction possible de cette fonctionnalité : + + [php] + birthDate = DateTime::createFromFormat('d/m/Y', $date); + } + + /** + * @Given /^que nous sommes le "([^"]*)"$/ + */ + public function queNousSommesLe($date) + { + $this->today = DateTime::createFromFormat('d/m/Y', $date); + } + + /** + * @When /^je calcule mon âge$/ + */ + public function jeCalculeMonAge() + { + $this->output = shell_exec(sprintf('php src/age.php --birthdate=%s --today=%s', $this->birthDate->format('Y-m-d'), $this->today->format('Y-m-d'))); + } + + /** + * @Then /^on me répond "([^"]*)"$/ + */ + public function onMeRepond($response) + { + if (false === strpos($this->output, $response)) { + throw new Exception; + } + } + } + + +Votre fonctionnel peut désormais ajouter autant d'exemples qu'il le souhaite, votre travail de traduction ne changera plus. + +> Utiliser des exemples est très simple avec Behat. + + +## Réutilisez vos précédents tests + +![ Les sources de cet exercice sont disponibles sur http://goo.gl/onk8x ] +(dev-qr-exo2.png) + + +Vous l'avez vu, la phase de traduction d'une phrase (étape) en code source peut parfois être longue. + +C'est pour cela que vous devez faire en sorte de faciliter votre travail : réutilisez au maximum vos définitions. Attention, je ne +vous parle en aucun cas ici de découper votre code comme vous le feriez dans une application (refactoring, respect des +principes SOLID, découpage des méthodes...). + +Non, la réutilisabilité dans les Contextes de traduction (fichiers `*Context.php`) ne passe pas par une réutilisation du code source, +mais bel et bien par une réutilisation de phrases. **En réalité, vous devez apprendre à faire du refactoring de phrases**. + +Cette tâche n'est pas simple au départ, mais est en réalité assez naturelle. Chaque phrase, qui correspond à un contexte, un +élément déclencheur ou un résultat attendu, peut être découpée en différentes étapes, qui **elles-mêmes pourront être décomposées +jusqu'à arriver à des expressions atomiques simples**. + +Prenez par exemple la phrase suivante : + + [gherkin] + Quand j'ajoute dans mon panier "télévision Sony" depuis le catalogue produit + +Cette phrase peut être découpée, par exemple : + + [gherkin] + Etant donné que je consulte le catalogue produit + Quand j'ajoute dans mon panier "télévision Sony" + +Qui elles-mêmes peuvent être découpées pour créer des expressions atomiques réutilisables : + + [gherkin] + Etant donné que je suis sur la page "/catalogue/produits" + Quand je coche "télévision Sony" + Et je clique sur "Ajouter au panier" + +Vous pouvez constater que les trois dernières expressions sont en réalité des étapes (web) atomiques réutilisables à l'infini. +Cela signifie qu'une fois que vous les aurez traduites, vous n'aurez plus jamais à revenir dessus, et vous pourrez vous en servir à votre gré. + +Vous pourriez désormais écrire, par exemple : + + [gherkin] + Etant donné que je suis sur la page "/home" + Etant donné que je suis sur la page "/mon-compte" + ... + +Cette démarche permet donc de faire du refactoring de phrases pour arriver à des expressions atomiques. Par chance, c'est +extrêmement simple à faire avec Behat. Regardez plutôt : + + [php] + Oubliez le refactoring de code. Vous devez apprendre à faire du refactoring de phrases. + +## Testez une application Web + +Vous savez désormais automatiser une recette fonctionnelle d'un applicatif. Cependant, dans le cadre d'une application internet, +la recette fonctionnelle consiste le plus souvent à parcourir des pages web, vérifier leur conformité (délivrent-elles le service +attendu ?), en soumettant un formulaire, en cliquant sur un lien... Bref, en surfant sur un site web. + +Heureusement, il est aujourd'hui très simple d'automatiser une recette fonctionnelle, quand bien même elle concerne une +application web. Allons-y ! + +Une fois de plus nous allons utiliser Behat. Plus précisément, nous allons utiliser une extension de Behat, appelée Mink, qui permet de +naviguer dans une application web et de lancer des tests automatisés lors de cette navigation. + +Concrètement, Mink permet une double approche : + ++ Exécuter des tests fonctionnels dans un navigateur virtuel (émulateur) ++ Exécuter des tests fonctionnels au sein d'un vrai navigateur, comme Chrome ou Firefox par exemple + +Il peut sembler étrange d'exécuter des tests dans un navigateur virtuel. Cependant, bien souvent le comportement fonctionnel +que l'on souhaite tester est en réalité disponible directement au sein des pages web, dans le HTML. *On préférera alors privilégier +les tests lancés dans un vrai navigateur uniquement lorsque le test porte sur des fonctionnalités complexes (en Javascript ou en Ajax +par exemple), ces tests étant en général beaucoup plus lourd et long à s'exécuter que les autres*. + +Pour commencer, installez `Mink` : + +### Installer Mink avec Composer + +Modifiez le fichier composer.json que vous avez préalablement créé, et ajoutez-y le code suivant : + + [json] + "require-dev": { + "behat/behat": "2.5.*@stable", + "behat/mink": "1.4.*@stable", + "behat/mink-extension": "~1.3", + "behat/mink-goutte-driver": "~1.1" + } + +Puis relancez Composer : + + [bash] + php composer.phar update + +### Installer Mink sous forme d'archive Phar + +Exécutez la commande suivante : + + [bash] + wget https://github.com/downloads/Behat/Mink/mink.phar + +Il vous suffit ensuite d'ajouter l'instruction suivantes dans vos fichiers PHP de Contexte : + + [php] + require_once 'mink.phar'; + +### Configurer Mink + +Maintenant, il suffit de configurer Behat pour lui indiquer que l'on utilise Mink. Mink étant une extension de Behat, C'est très simple. +Il suffit de créer le fichier `behat.yml` à la racine de votre projet : + + [yaml] + default: + extensions: + Behat\MinkExtension\Extension: + base_url: http://url-de-votre-site.fr + goutte: ~ + +Voilà, Mink est installé ! + +Rappelez-vous, le travail de Spécification fonctionnelle passe par l'utilisation d'expressions, elles-mêmes organisés autour +d'une grammaire : Gherkin. Mink a ceci d'intéressant qu'il propose nativement un panel assez large d'expressions qui concernent +un navigateur, que vous pouvez donc réutiliser (`comme "Quand je vais sur "http://..."`, ou encore `Quand je remplis "Votre prénom" avec +"Jean-François"`...). + +Un besoin exprimé de la façon suivante sera donc automatiquement compris par Behat lorsque Mink est installé: + + [gherkin] + Scenario: s'identifier au sein de l'application + Etant donné que je suis sur "/accueil" + Quand je suis le lien "Me connecter" + Et que je remplis "Identifiant" avec "Jean-François" + Et que je remplis "Mot de passe" avec "azerty" + Et que je clique sur "Valider" + Alors je dois voir "Bienvenue Jean-François" + +Pratique non ? Constatez que vous n'avez même pas besoin de parler de nom (attribut `name`) des champs HTML ; non, Behat +fera le lien pour vous entre les `label`, `title`, `class`... de vos champs de formulaire et l'expression que vous avez utilisée. + +Au fait, avez-vous lancé Behat pour vérifier que cela fonctionne ? Allez-y ; Utiliser Mink ne change rien à l'utilisation de Behat, +il suffit comme avant d'exécuter la commande suivante : + + [bash] + php ./bin/behat + +Bien entendu, il va souvent arriver que vous ayez besoin de gérer des cas plus complexes que ce qu'il est possible de faire avec +les expressions disponibles nativement dans Mink. + +Dans ce cas, il va s'agir, ni plus ni moins, de piloter votre navigateur (quel qu'il soit). Un très large éventail +de méthodes sont disponibles : + + [php] + getMink()->getSession(); + $page = $session->getPage(); + + $button = $page->find('css', '.class-css-du-bouton'); + $button->click(); + } + +Notez que désormais notre classe n'hérite plus de `BehatContext` mais de `MinkContext`. + +Examinons ce code ensemble. Tout d'abord, nous récupérons la page courante affichée par le navigateur : + + [php] + $session = $this->getMink()->getSession(); + $page = $session->getPage(); + + +Ensuite, il suffit de récupérer un élément HTML dans la page, puis d'exécuter l'action souhaitée sur cet élément (ici un clic, +mais on pourrait imaginer saisir une valeur, le cocher...) : + + [php] + $button = $page->find('css', '.class-css-du-bouton'); + $button->click(); + +Vous avez bien vu : la méthode `find()` permet de récupérer des éléments en utilisant des sélecteurs CSS. Ce sont les mêmes +que ce que vous utilisez dans vos feuilles de style CSS. Attention, les pseudo-styles (':hover', etc) ne sont pas supportés. +Bien entendu, il est également possible de récupérer des éléments HTML en utilisant des expressions xPath. La +[documentation de Mink est assez complète](http://mink.behat.org/#xpath-selectors) sur ce sujet. + +Il ne vous reste plus qu'à traduire les expressions fonctionnelles de vos clients en différentes actions au sein +d'un navigateur, et vous saurez traiter alors la majorité des cas. + +Sachez également que Mink permet très facilement de piloter un vrai navigateur. C'est utile lorsque l'on souhaite, par exemple, tester +des fonctionnalités qui nécessite un environnement Ajax ou JavaScript. Dans ce cas, juste au dessus des +Scénarios ou des Fonctionnalités concernées, il suffit d'ajouter le tag `@javascript`. Ce tag indiquera à Behat que vous souhaitez, dans ces +cas spécifiques, lancer un vrai navigateur pour les tests. Bien entendu, dans ce cas, la machine qui exécute les tests doit posséder un +environnement graphique (comme c'est le cas pour les ordinateurs de Bureau). + + +Lorsque l'on souhaite piloter un vrai navigateur, Behat délègue en réalité le travail à des outils spécialisés, comme `Sahi`, ou encore +comme `Selenium`. Dans ce cas, ces outils doivent être installés sur votre machine. La procédure pour les installer est en +général très simple, et est bien expliquée dans la [documentation officielle](http://mink.behat.org/#different-browsers-drivers). + +Voici la configuration à ajouter dans le fichier `behat.yml` pour indiquer que vous souhaitez utiliser Chrome par exemple, grâce au driver Sahi : + + [yaml] + default: + extensions: + Behat\MinkExtension\Extension: + base_url: http://url-de-votre-site.fr/ + goutte: ~ + default_session: sahi + javascript_session: sahi + browser_name: chrome + sahi: + host: localhost + port: 9999 + +Voici celle que vous pourriez utiliser pour piloter Firefox : + + [yaml] + default: + extensions: + Behat\MinkExtension\Extension: + base_url: http://url-de-votre-site.fr/ + goutte: ~ + default_session: sahi + javascript_session: sahi + browser_name: firefox + sahi: + host: localhost + port: 9999 + + +Vous avez désormais toutes les connaissances requises pour commencer à tester automatiquement un projet web. Pourquoi ne pas vous +entraîner par exemple sur une petite application web de guichet bancaire ? + +![ La correction et les sources de cet exercice sont disponibles sur http://goo.gl/LISNg ] +(dev-qr-exo3.png) + +Je vous propose de vous entraîner en traduisant le besoin fonctionnel suivant : + + [gherkin] + #language: fr + Fonctionnalité: Gérer un compte bancaire + Afin de gérer mon compte bancaire + En tant qu'utilisateur connecté + Je peux ajouter ou retirer de l'argent sur mon compte + + Contexte: + Etant donné que je suis connecté en tant que "jean-françois" + Et que j'ai "50" euro + Et je suis sur "/" + + Scénario: Consulter mes comptes + Alors je devrais voir "Vous avez 50 euro sur votre compte" + + Plan du Scénario: Ajouter de l'argent + Etant donné que j'ai "" euro + Quand je sélectionne "" depuis "Operation" + Et je remplis "Montant" avec "" + Et je presse "Go" + Alors je devrais voir "Vous avez euro sur votre compte" + + Exemples: + | operation | montantInitial| montant | montantFinal | + | Ajouter | 50 | 10 | 60 | + | Ajouter | 50 | 20 | 70 | + | Ajouter | 50 | 5 | 55 | + | Ajouter | 50 | 0 | 50 | + | Retirer | 50 | 10 | 40 | + | Retirer | 50 | 20 | 30 | + | Retirer | 50 | 30 | 20 | + + Scénario: Les découverts sont interdits + Etant donné que j'ai "50" euro + Quand je sélectionne "Retirer" depuis "Operation" + Et je remplis "Montant" avec "60" + Et je presse "Go" + Alors je devrais voir "Vous avez 50 euro sur votre compte" + Et je devrais voir "Les découverts ne sont pas autorises" + +A vous de jouer ! + +> Mink permet facilement de piloter un navigateur pour tester vos applications web. diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/dev-comprenez-la-demande-du-client.md b/data/doc-prince-book-generation/doc/tome2/Contents/dev-comprenez-la-demande-du-client.md new file mode 100644 index 0000000..f6d21be --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Contents/dev-comprenez-la-demande-du-client.md @@ -0,0 +1,271 @@ +# Comprenez (enfin!) ce que votre client vous demande + + +Votre client ne parle pas la même langue que vous. Comment comprendre ce qu'il vous demande dans ce cas ? +Certains vous répondront : "utilisez UML". D'autres vous diront qu'il faut demander à +votre client de s'exprimer sous forme de "si...alors". J'imagine que cela doit être possible +dans certain cas. Mais dans la majorité des situations, exiger de vos clients qu'ils apprennent l'UML, +ou leur demander de formaliser leur pensée sous forme de logique booléenne, est un contre-sens ! +Ils n'y arriveront pas efficacement : ce n'est pas leur métier, et probablement pas leur mode de pensée. + +C'est pour cela qu'il existe ce que l'on appelle le "Développement piloté par le Comportement" +("Behavior Driven Development", ou "BDD" en anglais). Ce qui se cache derrière ce terme barbare est +en réalité très simple : il s'agit d'une démarche pour faciliter la communication entre fonctionnels + et techniques, démarche soutenue par l'utilisation d'une langue et d'une grammaire commune. + +Cette grammaire commune (appelée "Gherkin") est simple, facile à apprendre, et compréhensible tout +autant par un informaticien que par votre voisin, votre conjoint(e)..., et par là-même par votre client. + +Utiliser cette grammaire vous offrira un certain nombre d'avantages : + ++ vous éviterez les quiproquos ++ les spécifications vous serviront de documentation ++ il sera plus facile d'identifier et de gérer les changements fonctionnels de dernière minute ++ vous saurez à tout moment ce qu'il vous reste à faire + +Attention, le Développement piloté par le comportement a un pré-requis majeur et non +négociable : *le client doit s'investir*. Il faudra que le client (ou son représentant fonctionnel) +soit disponible pour répondre à chacune de vos questions, en permanence. Si personne autour de vous +n'est prêt à s'investir pour exprimer le besoin clairement, oubliez l'idée de pratiquer le Développement +piloté par le comportement : vous n'aurez pas les moyens d'y parvenir. + +> Gherkin est une grammaire accessible à tous pour structurer l'expression de besoin + +## Le besoin doit être exprimé par des Fonctionnalités + +Concrètement, comment démarrer ? La première tâche revient à votre client. C'est lui qui +va devoir recenser la liste des Fonctionnalités qu'il souhaite voir dans son application. + +Pour cela, il va devoir identifier chaque fonctionnalité par un titre simple, court, explicite et unique : + + [gherkin] + # Le client va distinguer les fonctionnalités en leur donnant un titre + Fonctionnalité: Avoir accès à la liste des animaux de compagnie + Fonctionnalité: Choisir un animal de compagnie + Fonctionnalité: Acheter un animal de compagnie + Fonctionnalité: ... + +Il est impératif qu'à cette étape les titres des fonctionnalités soient clairs et explicites. + +Ensuite, il va lui suffire de contextualiser un peu cette fonctionnalité. Par exemple, il devra +indiquer qui est le bénéficiaire de cette fonctionnalité : + + + [gherkin] + En tant que vendeur + En tant que acheteur + En tant que propriétaire de la boutique + En tant que ... + +Vous aurez donc désormais connaissance de l'acteur bénéficiaire de la fonctionnalité. Il +reste encore à identifier le service offert par la fonctionnalité. C'est cela que vous aurez, finalement, à programmer : + + [gherkin] + Je veux acheter un animal de compagnie + Je veux connaître la liste des animaux de compagnie de mon magasin + Je veux connaître la liste des vendeurs + Je veux ... + +Pour lever toute ambiguïté, il est important pour vous de comprendre quel est le bénéfice +de la fonctionnalité que vous aller développer : à quoi sert-elle vraiment ? + +Après tout, c'est indispensable de comprendre et de connaître le pourquoi de votre métier. +Lorsque vous travaillez, le fruit de votre travail est vraiment utile ; et il est toujours gratifiant +de voir que l'on a rendu service à quelqu'un. C'est pour cela qu'il est important que le +client exprime le bénéfice obtenu par la fonctionnalité : + + [gherkin] + De telle sorte que je puisse repartir avec un nouveau compagnon + De telle sorte que je sache s'il me faut renouveler le stock + De telle sorte que je puisse organiser les congés de chacun + De telle sorte que ... + +Chaque fonctionnalité peut donc être exprimée en quatre lignes, très simples et compréhensibles par tous : + + + [gherkin] + Fonctionnalité : Connaître la liste des animaux de compagnie disponibles + En tant que vendeur + Je veux connaître la liste des animaux de compagnie disponibles à la vente + De telle sorte que je sache s'il me faut renouveler le stock + +Pratique non ? Cette manière d'exprimer les fonctionnalités (dénommée "Gherkin"), est +simple, mais surtout est compréhensible par tous. C'est le rôle de votre client (ou fonctionnel) +de recenser l'ensemble des fonctionnalités du projet de cette manière. +Mais, vous vous en doutez, cela ne suffit pas. Il va falloir maintenant comprendre en détail +chaque fonctionnalité ; c'est un travail que vous ferez en commun + +> Gherkin permet de décrire des fonctionnalités + +## Vous êtes un journaliste : interviewez ! + +Chaque Fonctionnalité peut donc être traduite en quatre petites phrases. Mais dans la majorité +des cas cela ne suffira pas : il reste des ambiguïtés, ce n'est pas très précis... + +C'est là que vous entrez en scène. A partir du moment où les fonctionnalités sont décrites +dans leurs grandes lignes, vous allez devoir les détailler. Oui, vous-même ! +Comment ? Tout simplement en prenant quelques instants la casquette d'un journaliste : +interviewez votre fonctionnel ! + +Attention, je vous parle d'une vraie interview. Prenez le temps, pour chaque Fonctionnalité, +de poser un maximum de questions pertinentes : "et qu'est-ce qui se passe quand ... ?" ; +ou encore "je ne comprend pas le bénéfice tiré de cette fonctionnalité pour l'utilisateur." + +Posez, donc, toutes ces questions. Et surtout planifiez systématiquement un moment, +avant chaque itération, où votre fonctionnel et l'ensemble des équipes techniques (oui, +toutes les personnes qui sont concernées), vont se réunir pour cette interview. + +J'insiste : il ne s'agit pas d'une réunion facultative : comment ferait-on sans cela pour être +sûr d'avoir bien compris la demande du client ? Le temps passé n'est pas perdu, loin de là : +c'est du temps de gagné sur la future maintenance, les futures recettes et les futurs va-et-vient +entre le client et les équipes techniques. + +L'objectif de ces interviews est de comprendre exactement, sans aucune ambiguïté possible, +ce qui vous est demandé fonctionnellement. Pour cela, il faudra découper les prochaines +Fonctionnalités que vous aurez à développer en "Scénarios". + + + +## Chaque Fonctionnalité peut être découpée en Scénarios + +Chaque Fonctionnalité peut être décrite en quatre points : + ++ un titre ++ un rôle ++ un service rendu ++ un bénéfice métier + + +Mais cela ne suffira probablement pas. Il reste des questions en suspens. C'est le rôle des +Scénarios de lever ces incertitudes. Ce sera d'autant plus vrai qu'ils seront rédigés en commun +avec le fonctionnel et les équipes techniques. + +Chaque Fonctionnalité peut contenir un ou plusieurs Scénarios, et chaque Scénario doit +pouvoir se distinguer facilement des autres par un titre : + + [gherkin] + Fonctionnalité : acheter un animal de compagnie + (...) + Scénario: acheter un animal de compagnie disponible à la vente + Scénario: tenter d'acheter un animal de compagnie qui est trop jeune + Scénario: tenter d'acheter un animal de compagnie qui est réservé + ... + +Votre travail de développeur va consister à rendre ces Scénarios effectifs dans +l'application. En identifiant les Scénarios réalisés et ceux qui ne le sont pas vous saurez +donc exactement ce qu'il vous reste à faire. + +Vous vous en doutez : un simple titre ne suffit pas ; il va falloir compléter chaque Scénario. +Posez par exemple cette question : "dans quel cadre ce scénario se situe t-il ?". Cela vous +permettra de connaître le *Contexte de votre Scénario*: + + [gherkin] + Scénario : ... + Etant donné que tel contexte existe + Etant donné que je souhaite acheter un chien + Etant donné qu'il n'y a plus un seul animal disponible + Etant donné ... + +Une application consiste généralement à réagir à des *événements*. Précisez-donc quels +sont ces événements : + + [gherkin] + Scénario : ... + (...) + Quand j'essaye d'acheter un chien + Quand je regarde quels sont les animaux disponibles + Quand ... + + + +Dès lors, il ne vous reste plus qu'à préciser le *Résultat attendu* lorsque cet événement +survient : + + [gherkin] + Scénario : ... + (...) + Alors je peux repartir avec un petit chien + Alors je dois être informé que le chien est trop jeune pour être + vendu + Alors ... + + +Cette syntaxe, simple, que l'on nomme Gherkin, va suffire dans la majorité des cas pour +que le fonctionnel puisse exprimer son besoin et surtout pour que vous puissiez comprendre +sans ambiguïté ce qui vous est demandé. + +Voici un exemple de Fonctionnalité : + + [gherkin] + Fonctionnalité : acheter un chiot + En tant que client du magasin + Je veux pouvoir acheter un chiot + De telle sorte que je puisse avoir un animal de compagnie + + Scénario : acheter un chiot disponible à la vente + Etant donné qu'un chiot est disponible en stock + Quand j'achète un chiot + Alors on me donne un chiot + + Scénario: acheter un chiot sevré + Etant donné qu'un chiot est trop jeune pour être vendu + Quand j'essaye d'acheter ce chiot + Alors je suis informé qu'il est trop jeune pour être vendu + +Pratique non ? + +## Demandez (exigez) des exemples précis + +Rappelez-vous que l'objectif du Développement piloté par le comportement est de lever +toutes les ambiguïtés. Pourtant, dans le scénario suivant, il reste des questions : + + [gherkin] + Scénario: acheter un chiot sevré + Etant donné qu'un chiot est trop jeune pour être vendu + Quand j'essaye d'acheter ce chiot + Alors je suis informé qu'il est trop jeune pour être vendu + +Ces questions sont nombreuses : + ++ à partir de quel âge un chiot peut-il être vendu ? ++ quel est l'âge du chiot que l'on essaye d'acheter ? ++ comment suis-je informé ? par quelle phrase ? + +Autant de questions auxquelles, si vous ne les posez pas maintenant, vous serez obligé de +répondre vous-même, avec tous les risques que cela comporte, notamment celui de devoir +recommencer votre développement ! + +Heureusement, il est tout à fait possible, avec Gherkin, de préciser des valeurs à utiliser. Il +suffit de les encadrer dans des guillemets : + + [gherkin] + Scénario : acheter un chiot sevré + Etant donné qu'un chiot ne peut être vendu avant qu'il n'ait "2 mois" + Et que "Médor le chien" a actuellement "1 mois" + Quand j'essaye d'acheter "Médor le chien" + Alors on doit me dire "Médor le chien est trop jeune !" + + + +Pas mal d'ambiguïtés sont levées non ? Et ça ne prend pas beaucoup plus de temps à écrire. +Maintenant, il peut arriver qu'un seul exemple ne suffise pas ; il en faudrait plusieurs. Pas +de problèmes, Gherkin vous permet d'utiliser des exemples facilement ; il suffit de les passer +sous forme de tableaux. Les variables sont alors à encadrer par les symboles "<" (plus petit que) +et ">" (plus grand que). + + [gherkin] + Plan du Scénario : acheter un chiot sevré + Etant donné qu'un chiot ne peut être vendu avant qu'il n'ait "2 mois" + Et que "" a actuellement "" + Quand j'essaye d'acheter "" + Alors on doit me dire " est trop jeune !" + + Exemples : + | nom-du-chien | age-du-chien | + | médor | 1 mois | + | rex | 1 mois et 3 jours | + | rantanplan | 15 jours | + +Remarquez qu'il ne s'agit plus dans ce cas d'un "Scénario" mais d'un "Plan du Scénario". + +Vous voici donc avec un besoin simple, découpé et exprimé clairement. A vous de jouer ! diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/dev-conclusion.md b/data/doc-prince-book-generation/doc/tome2/Contents/dev-conclusion.md new file mode 100644 index 0000000..4a57208 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Contents/dev-conclusion.md @@ -0,0 +1,23 @@ +# Le mot de la fin : le pouvoir du canard ! + +Connaissez-vous le "Duck Typing" (traduit par "typage canard") ? La légende veut que le poète James Whitcomb Riley soit à l'origine de cette expression : + +> Si je vois un animal qui vole comme un canard, cancane comme un canard, et nage comme un canard, alors j'appelle cet oiseau un canard + +Ce principe peut s'appliquer au test logiciel : si un module fonctionnel semble avoir le comportement attendu, possède l'apparence souhaitée, +ne génère pas d'erreur, est utilisable facilement, a été utilisé par plusieurs personnes... Alors on peut sans risque penser que +ce module fonctionnel est correct et cohérent. + +Cette analogie est importante car elle permet de comprendre que **tout ne peut (et ne doit) pas être testé par un logiciel**. +Traduire un besoin fonctionnel en tests automatisés prend du temps ; et ce temps, en général, vous ne l'aurez pas. Il faut donc +faire des choix : ne testez pas tout, ayez parfois confiance au travail de l'humain, et concentrez-vous sur ce qui est essentiel +ou sensible dans l'application que vous avez à développer. + +N'oubliez pas non plus que l'ensemble des pratiques que nous avons évoqué concernent avant tout une démarche. **Le développement +piloté par le Comportement est une démarche de travail avant de reposer sur des outils**. Non, les outils ne comptent pas, seule la communication +entre les différents acteurs d'un projet est essentielle. + +Enfin, et c'est le plus important, rappelez-vous qu'**un projet informatique consiste à produire un service qui sera utilisé +par des humains** (utilisateurs finaux). C'est à ces personnes qu'il faut penser en priorité. Toutes les pratiques, toutes les recettes +fonctionnelles et techniques qui peuvent être mises en place dans un projet ne consistent qu'à optimiser et à améliorer le +service rendu par un Produit. A vous de faire en sorte que ce Produit soit le plus efficace possible. \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/dev-le-client-ne-sait-pas-ce-quil-veut.md b/data/doc-prince-book-generation/doc/tome2/Contents/dev-le-client-ne-sait-pas-ce-quil-veut.md new file mode 100644 index 0000000..fac5d37 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Contents/dev-le-client-ne-sait-pas-ce-quil-veut.md @@ -0,0 +1,194 @@ +# Le client ne sait pas ce qu'il veut. Sauf si... vous communiquez. + + + +## Le client doit vous fournir sa Vision +J'ai souvent été confronté, en tant que développeur, et sur des projets de toutes tailles +(très grosses applications financières, petits intranet, sites web...) à cette situation : des +clients étaient incapables de me décrire ce qu'ils voulaient que je réalise pour eux. + +A chaque fois, dans ce cas, il m'a fallu moi-même interpréter leurs souhaits, à partir de captures d'écrans de +sites concurrents, de discussions interminables... Et pour un résultat pas toujours très heureux. + +A quoi la faute ? Au client ? Pas forcément : après tout, ce n'est pas parce qu'on est patron d'entreprise +ou que l'on a un besoin bien précis que l'on sait comment résoudre ce besoin, ou l'exprimer clairement. + +Est-ce la faute du développeur, qui semble incapable de comprendre ce qu'on lui demande ? Personnellement +j'ai toujours essayé de faire de mon mieux, même si je suis conscient de ne pas avoir toujours été à la hauteur. +Et je crois que la plupart des développeurs font de même. + +Alors ? Et si personne n'était en tort ? En réalité, la plupart des projets informatiques échouent car +ils ne sont pas motivés par une Vision. Certes on veut délivrer un super site web, avec plein de fonctionnalités +qui vont tuer la concurrence, on veut faire mieux que son voisin... Mais on oublie l'essentiel : un projet doit +voir un but. + +Quel est ce but ? Peu importe : changer le monde, rendre service à une catégorie de personne, se faire de +l'argent... Tout est bon à prendre, du moment que cet objectif, cette Vision, reste le seul et unique maître +du projet. + +On le comprend bien alors : ce n'est pas parce qu'un concurrent propose un service qu'il faut le recopier. +Non, on propose un nouveau service parce qu'il sert la Vision du projet. + +Vous l'aurez compris : la Vision est essentielle, et c'est justement le rôle de votre client de la fournir. +Sans Vision, le projet est condamné à être un échec, ou au mieux une semi-réussite. + +Votre client doit impérativement vous fournir cette Vision. S'il en est incapable, insistez pour qu'il soit +aidé : faites lui lire le premier tome de ce livre, faites le coacher par une société spécialisée... +Sans cela, vous ne pourrez pas travailler efficacement avec lui et ne prendrez pas autant de plaisir +que vous le méritez dans votre travail. + +> La plupart des projets informatiques échouent car ils ne sont pas motivés par une Vision. + + +## Votre Client ne parle pas la même langue que vous + +Vous l'avez vécu : votre client et vous ne vous comprenez pas toujours. Pour vous, un "réseau social" +c'est une plate-forme communautaire, avec des amis, relations, des groupes, des flux de messages... +Bref c'est Facebook. + +Pour votre client, un "réseau social" c'est (par exemple) un espace où des employés font des demandes +de documents, découvrent les actualités de l'entreprise, échangent des informations sur les commandes... +Bref, c'est un intranet. + +Attendez... Mais comment donc voulez-vous réussir à satisfaire votre client si vous n'employez +pas le même vocabulaire sur des choses si fondamentales ? + +Ajoutez à cela que votre client va toujours avoir tendance à faire deux choses : + ++ employer des acronymes, sigles et autre vocabulaire fonctionnel ++ employer des termes techniques ("base de données", "formulaire", "sauvegarder") sans savoir précisément ce qu'ils signifient + +La première chose à faire lorsque vous démarrez un projet avec un client, c'est donc de vous +créer un vocabulaire commun. Il va falloir inventer une nouvelle langue, qui ne laisse plus la +place aux ambiguïtés, où chaque mot n'a qu'une seule signification. + +J'ai une mauvaise nouvelle pour vous : ce nouveau vocabulaire, c'est à vous de l'apprendre, pas +à votre client. Cela vous évitera des débats interminables et vous permettra de mieux comprendre +le besoin du Client. + +Si votre client, lorsqu'il dit "bus", parle en réalité d'un véhicule avec des ailes et que vous +retrouvez dans un aéroport, ne lui dites pas qu'il a tort. Non, désormais, lorsque vous parlerez +avec lui, vous emploierez le mot "bus" pour désigner ce que LUI entend par "bus". + +Bien entendu, ce vocabulaire commun, vous devez le constituer ensemble. Si votre client parle de +"bus", et que c'est important dans le projet, discutez-en ensemble, et mettez noir sur blanc une +définition simple de ce qu'il entend par là. + +Faites de même pour chaque expression que votre client emploie régulièrement. De cette manière vous +n'aurez plus aucune ambiguïté dans votre quotidien. + +C'est le postulat de base du Développement piloté par le Comportement : vous devez élaborer avec votre +client une Langue Commune. + + + +## Engagez-vous à livrer régulièrement + +Quoi de plus frustrant, après de longues semaines de travail acharné, d'entendre un si triste +"Mais ce n'est pas du tout ce que j'avais demandé" ? + +Et même un simple "ne pourrait-on pas juste changer..." peut s'avérer totalement démoralisant, +compte-tenu de tout ce qu'il faudra refaire techniquement pour gérer cette demande, et sachant +surtout combien cela aurait été simple si seulement on vous avait prévenu quelques semaines à l'avance. + +Comment éviter d'être touché par ce malheureux constat d'échec, ou de semi-réussite ? + +La réponse est en théorie simple : il suffit de livrer son travail très régulièrement au client, de sorte qu'il +puisse rapidement s'apercevoir le plus tôt possible que le projet n'est pas parti dans la direction +à laquelle il s'attendait. + +Cette démarche est celle des méthodes agiles, particulièrement de Scrum. Le principe est le suivant : +plutôt que de livrer votre projet une bonne fois pour toute (lorsque le travail est terminé), vous vous engagez à livrer des +bouts de fonctionnalités très régulièrement (une fois toutes les deux semaines par exemple). + +C'est ce qu'on appelle le développement par itérations, par opposition au cycle en V. Vous développez +grossièrement une fonctionnalité, puis vous l'affinez, l'affinez encore si besoin, jusqu'à arriver au +résultat final. De cette manière le client peut réorienter le projet à chaque itération sans que n'en +soyez affecté négativement. + +Imaginez, par exemple, que votre travail consiste à peindre le célèbre tableau La Joconde. Avec la +méthodologie classique, votre devriez travailler de manière à finir chaque fonctionnalité de bout +en bout, puis, une fois qu'elle est entièrement terminée, passer à la suivante. + + +![ Méthodologie classique : le travail et découpé en lots, chaque lot est totalement réalisé avant de passer à la suite. Le changement fonctionnel en cours de route est difficile. ] +(mona-lisa-incremential.jpg) + +Avec les méthodes agiles la démarche est inverse : vous esquissez d'abord les traits de ce que vous avez +à développer, afin de permettre au client d'avoir un aperçu du produit final et de bénéficier rapidement +du feedback des utilisateurs, puis vous affinez, selon la priorité de chaque fonctionnalité : une +fonctionnalité plus complète par là, un autre lors de l'itération suivante... + +![Méthodologie agile : le tableau s'affine petit à petit. Le changement fonctionnel est facile, même en cours de route ] +(mona-lisa-iterative.jpg) + + +Bien entendu, cela ne va pas sans un changement des méthodologies de travail : il va falloir découper +les fonctionnalités pour faire en sorte qu'elles puissent être développées rapidement, il faut optimiser +les recettes (pourquoi ne pas en profiter pour utiliser des tests automatisés, comme des tests +unitaires ?)... + +Mais, croyez-moi, ces efforts valent la peine : non seulement les relations avec votre client / patron +seront de plus en plus saines (le projet devient totalement transparent pour tous), mais en plus vous +arriverez vite à livrer du code fonctionnel régulièrement. + +Et ô combien cela fait plaisir de voir que l'on avance ! Cela vous permettra même de gérer plus +facilement les demandes de changements fonctionnels : il est toujours plus facile de revenir sur +deux semaines de travail que sur trois mois, et c'est bien moins démoralisant. + +Je vous conseille même de faire quelque chose qui peut sembler assez étrange : à chaque fois que vous +livrerez un lot fonctionnel, n'hésitez pas à présenter, pendant une heure ou deux, le fruit de votre +travail à votre client. Oui, comme un commercial ! Prenez même un vidéo-projecteur. + +Calez systématiquement une réunion à chaque fin d'itération, et prenez la parole : montrez ce que +vous avez fait, et soyez-en fier ! Après tout, si vous avez donné le meilleur de vous même, il est +légitime que tout le monde sache de quoi il était question. + +> Prenez la parole ! N'entendez plus jamais le fameux "Ce n'est pas ce que j'avais demandé". + + + + + +## Adoptez le point de vue de l'utilisateur final + +Chaque projet informatique, qu'il s'agisse d'un site web, d'un intranet, d'une application... tout +projet dessert un objectif : rendre service à quelqu'un. Il faut vous mettre dans la peau de cette personne. + +Lorsque vous développez une boutique eCommerce, le service est rendu à un potentiel acheteur; lorsque +vous développez un intranet, ce sont les employés qui utiliseront l'intranet qui sont les bénéficiaires +du service. + +Dans les deux cas, vous aurez besoin de comprendre comment le Produit va être utilisé. Finalement, à quoi +sert cette fonctionnalité ? Pourquoi est-elle utile ? Comment va t-elle rendre service ? Va t-elle faciliter +la vie de l'utilisateur ? Va t-elle lui permettre de réaliser quelque chose de nouveau ? Lui faire gagner +du temps ? Bref, quel est le bénéfice métier de la fonctionnalité que je vais développer ? + +Pourquoi se poser ces questions ? Tout simplement pour prendre plus de plaisir dans votre travail, et pour +être plus efficace. Tant que ça ? Oui oui... + +Après tout, si vous connaissez les personnes qui vont utiliser ce que vous êtes en train de développer, vous +pouvez discuter avec elles, adopter leur vocabulaire, et donc profiter directement de leur feedback. Petit à +petit, ce feedback sera de plus en plus positif ; ce sera alors de plus en plus agréable de voir que les gens +sont contents de ce que vous leurs offrez. + +Cela vous permettra aussi de quitter à l'occasion la casquette de développeur pour, pourquoi pas, proposer +des améliorations, discuter de l'orientation du projet... Si vous savez comment va être utilisé le Produit, +qu'est-ce qui vous empêche d'essayer de l'améliorer ? + +Changer de casquette peut parfois être difficile. Vous pouvez très bien travailler sur des projets dont +vous n'avez rien à faire ; c'est triste, mais ça arrive fréquemment. Il arrive de devoir travailler sur +un projet pour des besoins alimentaires. C'est légitime. Mais autant faire en sorte que même ces projets, +alimentaires, vous apportent de la satisfaction humaine. Adopter la vision de l'utilisateur final vous +permettra toujours d'échanger : échanger avec votre client, les utilisateurs finaux, vos collègues... + +Il y a quelques temps, j'ai lancé un sondage auprès des développeurs de la société que j'ai accompagnée. +Une des questions était à peu près la suivante : "Prenez-vous plaisir dans votre travail?". +Près d'un quart des réponses étaient négatives. Quel dommage ! + +N'oubliez pas que vous travaillez dans un monde d'humains : plus vous interagirez avec eux, plus vous +fournirez un travail qui leur procurera satisfaction, plus vous même éprouverez du plaisir à travailler. +Le métier de développeur offre une chance unique : il permet de prendre plaisir en travaillant ; autant +en prendre le maximum ! + +> Changez de casquette pour prendre le maximum de plaisir dans votre métier de développeur. diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/dev-le-dev-est-un-plaisir.md b/data/doc-prince-book-generation/doc/tome2/Contents/dev-le-dev-est-un-plaisir.md new file mode 100644 index 0000000..0f8fca6 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Contents/dev-le-dev-est-un-plaisir.md @@ -0,0 +1,21 @@ +# Le développement est un plaisir +"Le client ne sait jamais ce qu'il veut". Comment pouvez-vous, dans ce cas, faire en sorte qu'il soit satisfait ? +Êtes-vous condamné à délivrer des logiciels, sites web ou applications dont le client ne sera pas réellement +satisfait ? + +Et vous-même, êtes-vous vraiment obligé de subir les contraintes fonctionnelles, de brider vos compétences +techniques ? Après tout, le métier de développeur est passionnant, et mérite qu'on y prenne le maximum de +plaisir ! Et c'est tellement décevant d'entendre cette fameuse phrase : "ce n'est pas ce que je voulais"... + +Il m'arrive de poser cette question aux personnes que je rencontre : "avez-vous, au moins une fois, +participé à un projet informatique où le projet a été livré à l'heure, et où, non seulement le client, +mais aussi le développeur, le testeur, l'intégrateur... ont été pleinement satisfaits et y ont pris plaisir ?". +A l'heure où j'écris ces lignes, seule une personne m'a répondu "oui". + +Quoi ?! Une seule personne ? Mais pourtant tous les projets devraient se passer comme ça. +Tous les projets devraient être livrés à l'heure, devraient être intéressants, enrichissants... +Toutes les personnes qui travaillent sur un projet devraient y prendre plaisir. Sinon, à quoi bon travailler ? + +L'émergence des méthodes agiles, ces dernières années, a permis de remettre l'humain, et ses valeurs, +au centre des projets. Je vous propose de découvrir ici l'autre versant, la face technique, des méthodes +agiles : le Développement piloté par le Comportement. diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/dev-optimisez-vos-tests-fonctionnels.md b/data/doc-prince-book-generation/doc/tome2/Contents/dev-optimisez-vos-tests-fonctionnels.md new file mode 100644 index 0000000..c861924 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Contents/dev-optimisez-vos-tests-fonctionnels.md @@ -0,0 +1,194 @@ +# Optimisez vos tests fonctionnels + +## Organisez vos Contextes de tests + +Vous l'avez vu, l'ensemble des définitions (traduction d'une fonctionnalité) est stocké au sein +d'une classe PHP, ici `FeatureContext`. + +Vous aurez très rapidement besoin d'organiser vos définitions de manière à vous y retrouver facilement. En effet, vous aurez +de plus en plus de définitions au fur-et-à-mesure que votre application grossira. + +Il est très simple avec Behat de créer plusieurs Contextes de définitions. Par exemple, pour une application bancaire, on pourrait créer +les contextes de définition suivants : + ++ Contexte de définitions pour les clients ++ Contexte de définitions pour les banquiers ++ Contexte de définitions pour les comptes bancaires ++ etc. + +Pour cela, il suffit simplement de créer un fichier PHP (et une classe) par Contexte de définition, par exemple : + ++ `ClientContext.php` ++ `BanquierContext.php` ++ `CompteContext.php` + +Puis de les initialiser dans le fichier principal `FeatureContext` : + + [php] + class FeatureContext extends BehatContext + { + public function __construct(array $parameters) { + $this->useContext('client', new ClientContext($parameters)); + $this->useContext('banquier', new BanquierContext($parameters)); + $this->useContext('compte', new CompteContext($parameters)); + } + } + +Désormais Behat utilisera l'ensemble des contextes disponibles pour traduire les expressions qu'il reçoit. + +Remarquez les alias de Contexte que nous avons utilisés, par exemple "mink" dans le code suivant : + + [php] + $this->useContext('mink', new MinkContext($parameters)); + +Ces alias vont vous permettre de récupérer facilement vos Contextes d'un Contexte à l'autre. Par exemple +pour récupérer votre contexte `mink` depuis le contexte `client`, il vous suffit d'utiliser le code suivant : + + [php] + $mink = $this->getMainContext()->getSubContext('mink'); + $session = $mink->getSession(); + +N'hésitez pas à découper vos Contextes de manière à les rendre facilement réutilisables par la suite. Cela vous évitera de mauvaises surprises +lorsque votre application grossira et que vous aurez à naviguer parmi plusieurs centaines de définitions. + +> Découpez et organisez vos Contextes de définition + +## Créez une couche d'isolation de l'IHM + +Une grande (très grande!) difficulté du développement piloté par le Comportement consiste à réussir +à s'abstraire de l'interface graphique. + +Imaginez par exemple la situation suivante : vous avez développé un site web pour votre client. Tout marche à merveille, +vous avez de très nombreuses définitions pour vérifier avec Behat que le besoin est bien implémenté... + +Votre client est tellement content que désormais il souhaite disposer d'une application mobile, identique au site web. +Vous voyez le problème ? Vous allez devoir récrire toutes les définitions où vous avez écrit des choses comme + + [php] + return array( + 'Etant donné que je suis sur "/login"' + ,'Quand je presse "Me connecter" + ); + +Car oui, sur mobile, il est fort probable que la manière de vérifier si le besoin est bien implémenté change radicalement par rapport +à un site web. + +Et même sans un changement aussi radical que le passage d'un site web à une application mobile, comment gérer facilement les changements +d'interface graphique : une `table` devient une `div`, le texte du bouton `Se connecter` devient `Connexion`... + +Le seul moyen d'éviter de devoir réécrire toutes ses définitions lorsqu'un changement d'interface survient (ce qui est le cas dans les +deux précédents exemples) est de **créer des couches d'isolation de l'interface**. + +Concrètement, c'est très simple : il suffit de créer des Contextes de définition spécialisé dans l'affichage des pages. On peut +imaginer par exemple un Contexte de définition consacré à l'affichage des pages Web, qui contiendra donc nos définitions liées à l'affichage : + + [php] + namespace View; + class WebContext extends BehatContext { + // (...) + } + +Et une autre, le jour où l'on en a besoin, consacré à l'affichage sur mobile : + + [php] + namespace View; + class MobileContext extends BehatContext { + // (...) + } + +Désormais, il suffit d'ajouter un paramètre de configuration spécifique dans le fichier `behat.yml`, que l'on utilisera +pour définir quel Contexte de définition l'on souhaite utiliser : + + [yaml] + default: + context: + parameters: + view: web # mobile | web ... + +Une simple condition dans le constructeur du Contexte principal suffit à nous permettre d'utiliser le bon Contexte : + + [php] + public function __construct(array $parameters) + { + if ($parameters['view'] == 'mobile') { + $this->useContext('view', new View\MobileContext($parameters)); + } else { + $this->useContext('view', new View\WebContext($parameters)); + } + } + + +Pour les applications assez conséquentes, il devient rapidement intéressant de créer même un Contexte +de définition par page (ou par lot fonctionnel) : + + [php] + $this->useContext('view.catalogue', new View\Web\CatalogueContext($parameters)); + $this->useContext('view.panier', new View\Web\PanierContext($parameters)); + +Cette pratique, que l'on pourrait désigner par Isolation de l'IHM, peut sembler complexe, mais est +en réalité le seul moyen efficace de péreniser les définitions au cours de la vie d'un projet. + +N'hésitez pas à consulter la documentation de [BehatPageObjectExtension](https://github.com/sensiolabs/BehatPageObjectExtension), +qui est une extension de Behat dédiée à cet usage. + +> Isolez ce qui concerne l'interface graphique dans des Contextes spécifiques + +## Exploitez les compte-rendus de tests + +Vous avez désormais un outil pour valider et vérifier qu'un besoin métier a bel et bien été implémenté. Pour l'instant, +vous avez exécuté vous-même Behat pour afficher ces informations dans un terminal. Vous vous en doutez, Behat va pouvoir +en faire bien plus. + +Pour commencer, il est tout à fait possible de générer une page web synthétique des résultats des tests. Pour cela, +il suffit d'exécuter Behat en spécifiant le `html` comme format de sortie : + + [bash] + php ./bin/behat --format html --out resultat.html + +Vous disposerez donc d'une page web, graphique, que vous pouvez fournir à votre fonctionnel pour lui +indiquer de manière claire ce qui est fait ou reste à faire parmi les spécifications. + +![ Le résultat de chaque Fonctionnalité et Scénario est visible dans une page web ](screen-export-html.jpg) + +Bien entendu, ce n'est pas tout. La majorité des outils de tests automatisés sont capables de générer des +fichiers de sortie dans un format standard (xml). Ces fichiers peuvent donc être fournis à des logiciels tiers, +capables de les traiter ou de les transformer selon leurs spécificité. + +Il est par exemple tout à fait possible d'intégrer Behat à une plate-forme d'intégration continue, comme Jenkins. +Une plate-forme d'intégration continue permet de délivrer en permanence du code source (déploiement continu), +c'est-à-dire de permettre à un site web d'être mis à jour très régulièrement. + +Indirectement, les plate-formes d'intégration continue (PIC) fournissent une vision d'ensemble sur un projet : les +bonnes pratiques sont-elles respectées ? Le code est-il fiable ? Le besoin est-il respecté ? Behat va pouvoir se greffer +au coeur de cette plate-forme et va ainsi fournir des indices précieux pour la pilotage du projet. + +Pour générer un résultat réutilisable par une plate-forme d'intégration continue, il suffit d'exécuter la commande suivante : + + [bash] + php ./bin/behat --format junit --out resultat.xml + +Pour aller plus loin avec les plate-formes d'intégration continue, je vous invite par exemple à consulter le site http://jenkins-php.org/. + +Il existe également des outils visuels, basés sur ce format de sortie, pour fournir aux fonctionnels une vraie interface graphique +pour rédiger les spécifications, mais aussi pour avoir une vue d'ensemble des résultats des implémentations de ces spécifications. + +![ Des outils graphiques pour visualiser et éditer les spécifications ](behat-wizard-home.jpg) + +Parmi ces outils, on peut mentionner par exemple [BehatWizard](http://halleck45.github.com/BehatWizardBundle/demo/behat/wizard/list.html) +ou [BehatViewer](https://github.com/behat-viewer/BehatViewer). + +Pour éviter de préciser manuellement quel format de sortie vous souhaitez utiliser lorsque vous lancez Behat, vous pouvez tout à +fait les préciser une bonne fois pour toute dans votre configuration, c'est-à-dire dans le fichier `behat.yml` : + + [yaml] + default: + formatter: + name: pretty,junit,html + parameters: + output_path: null,junit,report.html + +Cette configation vous permet par exemple d'afficher le résultat dans votre terminal, mais aussi générer des fichiers +de compte-rendu en HTML et en XML. + +> Multiplier les formats de sortie de compte-rendus permet de multiplier les usages + diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/dev-votre-code-doit-refleter-le-besoin-fonctionnel.md b/data/doc-prince-book-generation/doc/tome2/Contents/dev-votre-code-doit-refleter-le-besoin-fonctionnel.md new file mode 100644 index 0000000..28a5df1 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Contents/dev-votre-code-doit-refleter-le-besoin-fonctionnel.md @@ -0,0 +1,152 @@ +# Votre code doit refléter le Besoin fonctionnel + +Vous le savez, il arrive très souvent qu'un besoin fonctionnel évolue au cours de +la vie d'un projet. Et, parfois, une évolution fonctionnelle, même mineure, peut +entraîner une refonte majeure d'un code source. + +C'est l'effet boule de neige : modifier un bloc de code va avoir un impact sur une +grande partie de code source, qui elle-même va concerner des domaines fonctionnels très variés, +entraînant des régressions fonctionnelles nombreuses et imprévisibles. + +Si vous l'avez vécu, vous savez que c'est quelque chose qui est totalement +incompréhensible pour une personne qui n'est pas développeur. Ce n'est pas de la mauvaise +volonté, non ; c'est simplement que, s'il est déjà difficile de saisir pleinement qu'écrire +du code source permet au final d'avoir une application qui marche, comprendre qu'il existe +dans le code source des interactions fines, complexes et non liées aux domaines fonctionnels est +une chose quasiment impossible pour un fonctionnel. + +Il faut donc trouver une solution pour répondre à ces deux questions fondamentales : + ++ comment assurer la cohérence des changements techniques face aux changements fonctionnels ? ++ comment permettre aux fonctionnels d'anticiper le coût technique d'un changement fonctionnel ? + +## Le Domain Driven Design + +C'est justement l'enjeu du Domain Driven Design de répondre à ces questions. L'objet de +cet ouvrage n'est pas de faire de vous des spécialistes du Domain Driven Design (ou DDD) en +quelques pages. Non, il existe des livres très bien conçus, que je ne peux que vous inviter à +lire *(cf. Domain Driven Design Vite Fait + DDD de Eric Envans) - Liens à mettre* + +Non, l'objectif ici est de comprendre qu'il existe des démarches de travail et de conception +logicielle qui permettent de faciliter la gestion du besoin fonctionnel. Et c'est là, justement, l'objet du DDD. + +Le DDD est une démarche d'échange, de conception et de développement, dont l'objectif est de calquer +le code source (code, base de données, ressources...) sur le besoin métier. + +Impossible ? Si, et même très profitable. Sitôt que vous aurez commencé à travailler de cette manière, +vous ne pourrez plus vous en passer. + +L'idée générale est d'élaborer une Langue commune (cette Langue dont nous avons déjà parlé) avec +le fonctionnel, puis de concevoir une cartographie fonctionnelle de l'application, afin +d'utiliser, dans le code source, uniquement des concepts présents dans le domaine fonctionnel. + +Dès lors, si cette démarche a été accomplie, il devient facile pour le développeur +d'identifier quelle proportion de code et quels modules techniques seront impactés par une +demande de changement fonctionnel : il lui "suffit" de superposer le nouveau besoin fonctionnel +sur l'ancien code source. + +De cette manière, toute modification technique devient représentative des modifications fonctionnelles : +une modification fonctionnelle mineure risque moins d'entraîner une refonte technique longue et coûteuse. +Et a contrario, il est totalement légitime qu'une évolution fonctionnelle lourde nécessite une refonte +tout autant importante du code source. + +De manière générale, il est important de prendre conscience que le travail du développeur est de répondre à +un besoin fonctionnel. Il est donc important de structurer son code, de quelque manière que ce soit, de +façon à répondre au mieux et le plus facilement à ce besoin. + +> Les impacts techniques doivent être à la mesure des changements fonctionnels + +## Les tests unitaires sont indispensables + +En informatique on adore les sigles ; connaissez-vous le VDD ? C'est un sigle à la mode qui désigne +le "var_dump driven development", ou autrement dit, le développement piloté par la fonction `var_dump()` de PHP. +Notez que ça marche aussi si vous faites un `console.log()` en JavaScript, ou dans n'importe quel langage... + +Pourquoi est-ce une expression à la mode ? Tout simplement parce qu'elle désigne une pratique de développement +vouée à l'extinction, qui consiste pour le développeur à afficher à l'écran les valeurs de certaines variables pour +en vérifier le contenu manuellement. + +La raison pour laquelle cette pratique est vouée à disparaître est simple : lorsque l'on vérifie à la main +une valeur d'une variable, c'est long, ennuyant, et on risque de faire des erreurs. + +En réalité, faire un var_dump() pour afficher une information à l'écran n'est ni plus ni moins qu'un contrôle sur +l'état d'une variable. Or il est très facile d'imaginer des robots dont la seule fonction est de s'assurer en permanence +de l'état d'une variable. Et ces robots, infatigables et extrêmement rapides, ne se tromperont jamais. + +Ces robots, ce sont les tests unitaires automatisés. Écrire des tests unitaires est, à ce jour, l'un des seuls moyens vraiment efficaces +pour faciliter la vie d'un développeur, fiabiliser son code et réduire les coûts de maintenance d'une application. + +Il existe dans tous les langages de programmation des outils qui facilitent la rédaction de tests unitaires : JUnit en Java, QUnit en JavaScript... +En PHP, deux outils se démarquent par leur fiabilité et leur grande communauté: [PHPUnit](http://phpunit.de), développé par Sebastian Bergmann, +et [atoum](http://docs.atoum.org), développé par Frédéric Hardy. Ces deux outils proposent une approche légèrement différente des tests unitaires, +mais sont tous les deux simples, efficaces et complets. + +De manière générale, rédiger un test unitaire consiste à dérouler un processus simple : + ++ mettre en condition un code que l'on souhaite tester : ce sont les conditions du test ++ exécuter une commande : il s'agit de ce que l'on souhaite tester ++ décrire quel doit être l'effet de la commande : il s'agit alors du résultat attendu + +Lorsque l'on affiche une variable à l'écran pour vérifier sa valeur, en réalité, tout ce que l'on fait c'est exiger que +cette variable ait telle ou telle valeur. En d'autres termes, *on fait une assertion sur la valeur de la variable*. +Et toute assertion peut être traduite en code source. + +On peut imaginer, par exemple, créer un test unitaire automatisé pour une fonction d'addition, qui utiliserait la +fonction PHP native `assert()` : + + [php] + $resultat = addition(5,5); + assert($resultat === 10); + +Ce code PHP générera une erreur si la fonction addition() ne renvoie pas "10". + +L'avantage d'utiliser un outil de tests automatisés sera de pouvoir organiser ses tests facilement et efficacement. Chaque +assertion valide sera affichée en vert, les erreurs seront remontées aux développeur en un instant. + + +![ Les tests unitaires permettent de s'assurent que le code fonctionne comme prévu](phpunit.png) + +Grâce aux tests automatisés, vous saurez à tout instant si vous venez de tout casser ou pas ; +ce sera d'autant plus facile de tout réparer... + +> Les tests unitaires sont le seul moyen efficace de fiabiliser un code source + +## Tester le besoin métier est indispensable + +Tester son code source est bien. C'est même déjà pas mal du tout ! Mais une application web est plus qu'un code source : c'est +un assemblage de *comportements* (code source) *vis-à-vis d'informations* (données, souvent issues d'une base de données), *pour +délivrer un message* (par exemple une page web), le tout exécuté dans un environnement spécifique (serveur), le tout +*vis-à-vis d'un utilisateur* (navigateur). + +On comprend vite du coup que ce n'est pas parce qu'un code source fonctionne de la manière attendue que l'application, elle aussi, +aura le comportement désiré. + +Bien plus, il est en réalité difficile de tester unitairement l'ensemble d'un code source : c'est long et coûteux, et cela +exige des changements importants dans les pratiques de développement (Développement piloté par les tests, génération de +tests aléatoires, etc.). + +C'est pour ces raisons qu'il existe différents niveaux de tests automatisés : + ++ les *tests d'intégration*, qui s'assurent de l'environnement donné (version de PHP, présence d'un module apache...) ++ les *tests de charge*, qui s'assurent de la performance d'une application ++ les *tests unitaires*, qui vérifient le comportement du code source ++ les *tests d'interface*, qui vérifient qu'une application a bien l'aspect attendu ++ les *tests fonctionnels*, qui s'assurent que l'application délivre bien un service métier à l'utilisateur + +Il existe bien entendu d'autres niveaux de tests (sécurité, cohérence...), mais ceux-ci sont les plus +répandus. + +Le dernier niveau de test (le test fonctionnel) est sans doute parmi les plus importants. Finalement, tout votre travail +consiste justement à faire en sorte que l'utilisateur tire un bénéfice fonctionnel d'une application. S'assurer que +ce bénéfice fonctionnel est bien présent est indispensable pour votre client. + +Mais justement, ce contrôle, autrement dit ce travail de Recette client, est long et laborieux. Il est source +d'erreurs, et surtout il est impossible de contrôler à la main, lors de chaque évolution fonctionnelle, que +l'ensemble de l'application fonctionne toujours de la manière attendue. + +C'est pour cela qu'il existe désormais des outils de tests fonctionnels automatisés. Ces outils sont +infatigables: ils traquent en permanence votre application à la recherche d'anomalies fonctionnelles. + +Nous verrons par la suite comment mettre en place ces outils. Mais avant de faire une recette fonctionnelle, +il faut déjà commencer par comprendre la demande initiale. Il est temps de découvrir comment faciliter la compréhension +d'une demande fonctionnelle. diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/behat-wizard-edit.jpg b/data/doc-prince-book-generation/doc/tome2/Contents/images/behat-wizard-edit.jpg new file mode 100644 index 0000000..d72484b Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/behat-wizard-edit.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/behat-wizard-home.jpg b/data/doc-prince-book-generation/doc/tome2/Contents/images/behat-wizard-home.jpg new file mode 100644 index 0000000..2757982 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/behat-wizard-home.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/cartographie-or.png b/data/doc-prince-book-generation/doc/tome2/Contents/images/cartographie-or.png new file mode 100644 index 0000000..583b0c1 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/cartographie-or.png differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/cartographie.png b/data/doc-prince-book-generation/doc/tome2/Contents/images/cartographie.png new file mode 100644 index 0000000..61bb666 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/cartographie.png differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/cover.jpg b/data/doc-prince-book-generation/doc/tome2/Contents/images/cover.jpg new file mode 100644 index 0000000..a908ead Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/cover.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/cover.old.jpg b/data/doc-prince-book-generation/doc/tome2/Contents/images/cover.old.jpg new file mode 100644 index 0000000..a908ead Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/cover.old.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-behat-exo-date-fail.jpg b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-behat-exo-date-fail.jpg new file mode 100644 index 0000000..ee8fa5d Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-behat-exo-date-fail.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-behat-exo-date-undefined.jpg b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-behat-exo-date-undefined.jpg new file mode 100644 index 0000000..a2b86e2 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-behat-exo-date-undefined.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo1-step1.png b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo1-step1.png new file mode 100644 index 0000000..8668632 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo1-step1.png differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo1-step2.png b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo1-step2.png new file mode 100644 index 0000000..13d1685 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo1-step2.png differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo2.png b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo2.png new file mode 100644 index 0000000..00933c4 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo2.png differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo3.png b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo3.png new file mode 100644 index 0000000..3f5c12f Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/dev-qr-exo3.png differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/mona-lisa-incremential.jpg b/data/doc-prince-book-generation/doc/tome2/Contents/images/mona-lisa-incremential.jpg new file mode 100644 index 0000000..01cfcfd Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/mona-lisa-incremential.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/mona-lisa-iterative.jpg b/data/doc-prince-book-generation/doc/tome2/Contents/images/mona-lisa-iterative.jpg new file mode 100644 index 0000000..e47a402 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/mona-lisa-iterative.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/phpunit.png b/data/doc-prince-book-generation/doc/tome2/Contents/images/phpunit.png new file mode 100644 index 0000000..8661767 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/phpunit.png differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/images/screen-export-html.jpg b/data/doc-prince-book-generation/doc/tome2/Contents/images/screen-export-html.jpg new file mode 100644 index 0000000..22c9a47 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Contents/images/screen-export-html.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Contents/license.md b/data/doc-prince-book-generation/doc/tome2/Contents/license.md new file mode 100644 index 0000000..7038908 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Contents/license.md @@ -0,0 +1,8 @@ +Ce livre est publié sous licence Creative Commons. + +Vous pouvez contribuer à son amélioration sur [Github](http://wwww.github.com/Halleck45/book-bdd) + +Copyright (c) Jean-François Lépine + +Ce livre de [Jean-François Lépine](http://communiquez.lepine.pro) est mis à disposition selon les termes de la [licence Creative Commons Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé](http://creativecommons.org/licenses/by-nc-sa/3.0/deed.fr). +Fondé(e) sur une œuvre à [https://github.com/Halleck45/livre-developpement-pilote-comportement](https://github.com/Halleck45/livre-developpement-pilote-comportement). \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/tome2/Resources/Templates/cover-small.jpg b/data/doc-prince-book-generation/doc/tome2/Resources/Templates/cover-small.jpg new file mode 100644 index 0000000..603881a Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Resources/Templates/cover-small.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Resources/Templates/cover.jpg b/data/doc-prince-book-generation/doc/tome2/Resources/Templates/cover.jpg new file mode 100644 index 0000000..360aa5c Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Resources/Templates/cover.jpg differ diff --git a/data/doc-prince-book-generation/doc/tome2/Resources/Templates/cover.pdf b/data/doc-prince-book-generation/doc/tome2/Resources/Templates/cover.pdf new file mode 100644 index 0000000..bdf3927 Binary files /dev/null and b/data/doc-prince-book-generation/doc/tome2/Resources/Templates/cover.pdf differ diff --git a/data/doc-prince-book-generation/doc/tome2/Resources/Templates/print/style.css b/data/doc-prince-book-generation/doc/tome2/Resources/Templates/print/style.css new file mode 100644 index 0000000..2e1d509 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Resources/Templates/print/style.css @@ -0,0 +1,83 @@ +body { + color: #333333; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + line-height: 20px; +} +p strong, li strong { + color:#008287; +} +.code { + background:#000; +} +.code { + color:#999; +} + +h1 { + +} +h2 { + background-color: #F7F7F9; + border: 1px solid #E1E1E8; + padding:10px; + margin-top:30px; +} +h3 { + font-size:14px !important; +} + +blockquote p { + font-size:18px; + background:#CCC; + color:#333; + width:80%; + font-style: italic; + margin-left:15%; + padding:10px; +} + +.item.toc .part.level-1 { + color:#F7F7F9; + background-color: #000; + border: 1px solid #E1E1E8; + padding:10px; + margin-top:30px; + font-size:26px; +} +.item.part h1 { + text-align:center; + font-size:70px; +} + + +/* --- gherkin --- */ +/*texte*/ +div.code.gherkin { + color:#C5C8C6; +} +/*commentaire*/ +div.code.gherkin .co1 { + color:#969896; +} +/*clef*/ +div.code.gherkin .co3 { + color:#DE935F; +} +/*Exemples*/ +div.code.gherkin .co4 { + color:#DE935F; +} +/*mot clef*/ +div.code.gherkin .kw1 { + color:#81A2BE; +} +/* +vert #B5BD68 +orange #DE935F +violet #B294BB +bleu #81A2BE +blanc #C5C8C6 +gris #969896 +jaune #B5BD68 +*/ \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/tome2/Resources/Templates/web/style.css b/data/doc-prince-book-generation/doc/tome2/Resources/Templates/web/style.css new file mode 100644 index 0000000..f4f144a --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/Resources/Templates/web/style.css @@ -0,0 +1,63 @@ +body { + color: #333333; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 14px; + line-height: 20px; +} + +.code { + background:#000; +} +.code { + color:#999; +} + +h2 { + background-color: #F7F7F9; + border: 1px solid #E1E1E8; + padding:10px; + margin-top:30px; +} +h3 { + font-size:14px !important; +} + +em { + font-size:12px; + color:#c20cb9; +} +.item.part h1 { + text-align:center; + font-size:40px; +} + +/* --- gherkin --- */ +/*texte*/ +div.code.gherkin { + color:#C5C8C6; +} +/*commentaire*/ +div.code.gherkin .co1 { + color:#969896; +} +/*clef*/ +div.code.gherkin .co3 { + color:#DE935F; +} +/*Exemples*/ +div.code.gherkin .co4 { + color:#DE935F; +} +/*mot clef*/ +div.code.gherkin .kw1 { + color:#81A2BE; +} +/* +vert #B5BD68 +orange #DE935F +violet #B294BB +bleu #81A2BE +blanc #C5C8C6 +gris #969896 +jaune #B5BD68 +*/ \ No newline at end of file diff --git a/data/doc-prince-book-generation/doc/tome2/config.yml b/data/doc-prince-book-generation/doc/tome2/config.yml new file mode 100644 index 0000000..b904f62 --- /dev/null +++ b/data/doc-prince-book-generation/doc/tome2/config.yml @@ -0,0 +1,72 @@ +book: + title: "Le développement piloté par le comportement" + subtitle: "Développeurs, faites-vous plaisir !" + author: "Jean-François Lépine" + edition: "First edition" + language: fr + publication_date: ~ + + generator: { name: easybook, version: 5.0-DEV } + + contents: + # available content types: acknowledgement, afterword, appendix, author, + # chapter, conclusion, cover, dedication, edition, epilogue, foreword, + # glossary, introduction, license, lof (list of figures), lot (list of + # tables), part, preface, prologue, title, toc (table of contents) + - { element: cover } + - { element: toc } + - { element: introduction, content: dev-le-dev-est-un-plaisir.md } + - { element: chapter, number: 1, content: dev-le-client-ne-sait-pas-ce-quil-veut.md } + - { element: chapter, number: 2, content: dev-votre-code-doit-refleter-le-besoin-fonctionnel.md } + - { element: chapter, number: 3, content: dev-comprenez-la-demande-du-client.md } + - { element: chapter, number: 4, content: dev-automatisez-votre-recette.md } + - { element: chapter, number: 5, content: dev-optimisez-vos-tests-fonctionnels.md } + - { element: chapter, number: 6, content: dev-conclusion.md } + + - { element: author, content: author.md } + - { element: license, content: license.md } + + + + editions: + ebook: + format: epub + highlight_code: false + include_styles: true + labels: ['appendix', 'chapter'] # labels also available for: "figure", "table" + theme: clean + toc: + deep: 1 + elements: ["appendix", "chapter", "part"] + + print: + format: pdf + highlight_code: true + include_styles: true + isbn: ~ + labels: ["appendix", "chapter"] # labels also available for: "figure", "table" + margin: + top: 25mm + bottom: 25mm + inner: 30mm + outter: 20mm + page_size: A4 + theme: Base + toc: + deep: 2 + elements: ["appendix", "chapter", "part"] + two_sided: true + + web: + format: html + highlight_code: true + include_styles: true + labels: ["appendix", "chapter"] # labels also available for: "figure", "table" + theme: clean + toc: + deep: 2 + elements: ["appendix", "chapter", "part"] + + website: + extends: web + format: html_chunked diff --git a/data/doc-prince-book-generation/patch/json.php b/data/doc-prince-book-generation/patch/json.php new file mode 100644 index 0000000..b883fca --- /dev/null +++ b/data/doc-prince-book-generation/patch/json.php @@ -0,0 +1,174 @@ + 'JSON', + 'COMMENT_SINGLE' => array(1 => '//'), + 'COMMENT_MULTI' => array('/*' => '*/'), + 'COMMENT_REGEXP' => array( + //Regular Expressions + 2 => "/(?<=[\\s^])(s|tr|y)\\/(?!\*)(?!\s)(?:\\\\.|(?!\n)[^\\/\\\\])+(? GESHI_CAPS_NO_CHANGE, + 'QUOTEMARKS' => array("'", '"'), + 'ESCAPE_CHAR' => '\\', + 'KEYWORDS' => array( + 1 => array( + //reserved/keywords; also some non-reserved keywords + 'break','case','catch','const','continue', + 'default','delete','do', + 'else', + 'finally','for','function', + 'get','goto', + 'if','in','instanceof', + 'new', + 'prototype', + 'return', + 'set','static','switch', + 'this','throw','try','typeof', + 'var','void' + ), + 2 => array( + //reserved/non-keywords; metaconstants + 'false','null','true','undefined','NaN','Infinity' + ), + 3 => array( + //magic properties/functions + '__proto__','__defineGetter__','__defineSetter__','hasOwnProperty','hasProperty' + ), + 4 => array( + //type constructors + 'Object', 'Function', 'Date', 'Math', 'String', 'Number', 'Boolean', 'Array' + ), + 5 => array( + //reserved, but invalid in language + 'abstract','boolean','byte','char','class','debugger','double','enum','export','extends', + 'final','float','implements','import','int','interface','long','native', + 'short','super','synchronized','throws','transient','volatile' + ), + ), + 'SYMBOLS' => array( + '(', ')', '[', ']', '{', '}', + '+', '-', '*', '/', '%', + '!', '@', '&', '|', '^', + '<', '>', '=', + ',', ';', '?', ':' + ), + 'CASE_SENSITIVE' => array( + GESHI_COMMENTS => false, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + 5 => true + ), + 'STYLES' => array( + 'KEYWORDS' => array( + 1 => 'color: #000066; font-weight: bold;', + 2 => 'color: #003366; font-weight: bold;', + 3 => 'color: #000066;', + 5 => 'color: #FF0000;' + ), + 'COMMENTS' => array( + 1 => 'color: #006600; font-style: italic;', + 2 => 'color: #009966; font-style: italic;', + 'MULTI' => 'color: #006600; font-style: italic;' + ), + 'ESCAPE_CHAR' => array( + 0 => 'color: #000099; font-weight: bold;' + ), + 'BRACKETS' => array( + 0 => 'color: #009900;' + ), + 'STRINGS' => array( + 0 => 'color: #3366CC;' + ), + 'NUMBERS' => array( + 0 => 'color: #CC0000;' + ), + 'METHODS' => array( + 1 => 'color: #660066;' + ), + 'SYMBOLS' => array( + 0 => 'color: #339933;' + ), + 'REGEXPS' => array( + ), + 'SCRIPT' => array( + 0 => '', + 1 => '', + 2 => '', + 3 => '' + ) + ), + 'URLS' => array( + 1 => '', + 2 => '', + 3 => '', + 4 => '', + 5 => '' + ), + 'OOLANG' => true, + 'OBJECT_SPLITTERS' => array( + 1 => '.' + ), + 'REGEXPS' => array( + ), + 'STRICT_MODE_APPLIES' => GESHI_MAYBE, + 'SCRIPT_DELIMITERS' => array( + 0 => array( + '' + ), + 1 => array( + '' + ) + ), + 'HIGHLIGHT_STRICT_BLOCK' => array( + 0 => true, + 1 => true + ) +); + +?> \ No newline at end of file diff --git a/data/doc-prince-book-generation/resources/bin/behat b/data/doc-prince-book-generation/resources/bin/behat new file mode 100644 index 0000000..cf50f7f --- /dev/null +++ b/data/doc-prince-book-generation/resources/bin/behat @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +dir=$(cd "${0%[/\\]*}" > /dev/null; cd "../vendor/behat/behat/bin" && pwd) + +if [ -d /proc/cygdrive ]; then + case $(which php) in + $(readlink -n /proc/cygdrive)/*) + # We are in Cygwin using Windows php, so the path must be translated + dir=$(cygpath -m "$dir"); + ;; + esac +fi + +"${dir}/behat" "$@" diff --git a/data/doc-prince-book-generation/resources/bin/behat.bat b/data/doc-prince-book-generation/resources/bin/behat.bat new file mode 100644 index 0000000..cdcbee7 --- /dev/null +++ b/data/doc-prince-book-generation/resources/bin/behat.bat @@ -0,0 +1,4 @@ +@ECHO OFF +setlocal DISABLEDELAYEDEXPANSION +SET BIN_TARGET=%~dp0/../vendor/behat/behat/bin/behat +php "%BIN_TARGET%" %* diff --git a/data/doc-prince-book-generation/resources/composer.json b/data/doc-prince-book-generation/resources/composer.json new file mode 100644 index 0000000..6d3c24c --- /dev/null +++ b/data/doc-prince-book-generation/resources/composer.json @@ -0,0 +1,12 @@ +{ + "require": { + "behat/behat": "2.4.*@stable", + "behat/mink": "1.4@stable", + "behat/mink-extension": "*", + "behat/mink-goutte-driver": "*" + }, + + "config": { + "bin-dir": "bin/" + } +} diff --git a/data/doc-prince-book-generation/resources/exercices/exo1-1/features/bootstrap/FeatureContext.php b/data/doc-prince-book-generation/resources/exercices/exo1-1/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..99db8a7 --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo1-1/features/bootstrap/FeatureContext.php @@ -0,0 +1,92 @@ +birthDate = new \DateTime(sprintf('%d-%d-%d', $year, $month, $day)); + } + + /** + * @Given /^que nous sommes le (\d+)\/(\d+)\/(\d+)$/ + */ + public function queNousSommesLe($day, $month, $year) + { + $this->today = new \DateTime(sprintf('%d-%d-%d', $year, $month, $day)); + } + + /** + * @Given /^je calcule mon âge$/ + */ + public function jeCalculeMonAge() + { + $this->output = shell_exec(sprintf('php src/age.php --birthdate=%s --today=%s', $this->birthDate->format('Y-m-d'), $this->today->format('Y-m-d'))); + } + + /** + * @Given /^je suis informé que j\'ai (\d+) ans$/ + */ + public function jeSuisInformeQueJAiAns($age) + { + if(!preg_match('!'.$age.' ans!', $this->output)) { + throw new Exception(); + } + } + + /** + * @Given /^je suis informé que je ne suis pas encore né$/ + */ + public function jeSuisInformeQueJeNeSuisPasEncoreNe() + { + if(!preg_match('!Vous n\'êtes pas encore né!', $this->output)) { + throw new Exception(); + } + } + + /** + * @Given /^on me souhaite un joyeux anniversaire$/ + */ + public function onMeSouhaiteUnJoyeuxAnniversaire() + { + if(!preg_match('!Joyeux anniversaire!', $this->output)) { + throw new Exception(); + } + } + +} diff --git a/data/doc-prince-book-generation/resources/exercices/exo1-1/features/calculer-age.feature b/data/doc-prince-book-generation/resources/exercices/exo1-1/features/calculer-age.feature new file mode 100644 index 0000000..bbed58b --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo1-1/features/calculer-age.feature @@ -0,0 +1,24 @@ +# language: fr +Fonctionnalité: Calculer l'âge d'une personne + En tant qu'utilisateur de l'application + Je veux connaître le nombre d'années écoulées entre deux dates + De telle sorte que je puisse connaître mon age + +Scénario: Calculer l'âge d'une personne depuis une date antérieure à aujourd'hui + Etant donné que je suis né le 06/07/1986 + Et que nous sommes le 20/09/013 + Quand je calcule mon âge + Alors je suis informé que j'ai 27 ans + +Scénario: Calculer l'âge d'une personne depuis une date postérieure à aujourd'hui + Etant donné que je suis né le 06/07/3013 + Et que nous sommes le 20/09/2013 + Quand je calcule mon âge + Alors je suis informé que je ne suis pas encore né + +Scénario: Calculer l'âge d'une personne dont c'est l'anniversaire aujourd'hui + Etant donné que je suis né le 06/07/1986 + Et que nous sommes le 06/07/2013 + Quand je calcule mon âge + Alors je suis informé que j'ai 27 ans + Et on me souhaite un joyeux anniversaire \ No newline at end of file diff --git a/data/doc-prince-book-generation/resources/exercices/exo1-1/src/age.php b/data/doc-prince-book-generation/resources/exercices/exo1-1/src/age.php new file mode 100644 index 0000000..e3f6221 --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo1-1/src/age.php @@ -0,0 +1,33 @@ +diff($today); + + + +if ($interval->invert == 1) { + // + // today < birthdate + echo "Vous n'êtes pas encore né."; +} else { + // + // age + echo sprintf('Vous avez %d ans. ', $interval->format('%y')); + + // + // Happy birthday ! + if (($birthDate->format('%d') == $today->format('%d')) + && ($birthDate->format('%m') == $today->format('%m'))) { + echo ' Joyeux anniversaire !'; + } +} diff --git a/data/doc-prince-book-generation/resources/exercices/exo1-2/features/bootstrap/FeatureContext.php b/data/doc-prince-book-generation/resources/exercices/exo1-2/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..749ea00 --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo1-2/features/bootstrap/FeatureContext.php @@ -0,0 +1,61 @@ +birthDate = DateTime::createFromFormat('d/m/Y', $date); + } + + /** + * @Given /^que nous sommes le "([^"]*)"$/ + */ + public function queNousSommesLe($date) + { + $this->today = DateTime::createFromFormat('d/m/Y', $date); + } + + /** + * @Given /^je calcule mon âge$/ + */ + public function jeCalculeMonAge() + { + $this->output = shell_exec(sprintf('php src/age.php --birthdate=%s --today=%s', $this->birthDate->format('Y-m-d'), $this->today->format('Y-m-d'))); + } + + /** + * @Given /^on me répond "([^"]*)"$/ + */ + public function onMeRepond($response) + { + if (false === strpos($this->output, $response)) { + throw new Exception; + } + } + +} diff --git a/data/doc-prince-book-generation/resources/exercices/exo1-2/features/calculer-age.feature b/data/doc-prince-book-generation/resources/exercices/exo1-2/features/calculer-age.feature new file mode 100644 index 0000000..4fd898d --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo1-2/features/calculer-age.feature @@ -0,0 +1,18 @@ +# language: fr +Fonctionnalité: Calculer l'âge d'une personne + En tant qu'utilisateur de l'application + Je veux connaître le nombre d'années écoulées entre deux dates + De telle sorte que je puisse connaître mon age + + Plan du Scénario: Calculer l'âge d'une personne + Etant donné que je suis né le "" + Et que nous sommes le "" + Quand je calcule mon âge + Alors on me répond "" + + Exemples: + | dateNaissance | dateDuJour | reponseAttendu | + | 06/07/1986 | 20/09/2013 | Vous avez 27 ans | + | 06/07/1985 | 20/09/2013 | Vous avez 28 ans | + | 26/11/2020 | 20/09/2013 | Vous n'êtes pas encore né | + | 06/07/1986 | 06/07/2013 | Vous avez 27 ans. Joyeux anniversaire | \ No newline at end of file diff --git a/data/doc-prince-book-generation/resources/exercices/exo1-2/src/age.php b/data/doc-prince-book-generation/resources/exercices/exo1-2/src/age.php new file mode 100644 index 0000000..f4645c4 --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo1-2/src/age.php @@ -0,0 +1,33 @@ +diff($today); + + + +if ($interval->invert == 1) { + // + // today < birthdate + echo "Vous n'êtes pas encore né."; +} else { + // + // age + echo sprintf('Vous avez %d ans.', $interval->format('%y')); + + // + // Happy birthday ! + if (($birthDate->format('%d') == $today->format('%d')) + && ($birthDate->format('%m') == $today->format('%m'))) { + echo ' Joyeux anniversaire !'; + } +} diff --git a/data/doc-prince-book-generation/resources/exercices/exo2/features/ajout-panier.feature b/data/doc-prince-book-generation/resources/exercices/exo2/features/ajout-panier.feature new file mode 100644 index 0000000..502e0fa --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo2/features/ajout-panier.feature @@ -0,0 +1,8 @@ +# language: fr +Fonctionnalité: Ajouter un produit dans mon panier + En tant que visiteur non connecté + Je veux pouvoir ajouter un produit dans mon panier + De telle sorte que je regrouper mes produits en vue d'un achat unique + + Scénario: Ajouter un produit dans mon panier + Quand j'ajoute dans mon panier "télévision Sony" depuis le catalogue produit \ No newline at end of file diff --git a/data/doc-prince-book-generation/resources/exercices/exo2/features/bootstrap/FeatureContext.php b/data/doc-prince-book-generation/resources/exercices/exo2/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..2a09da1 --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo2/features/bootstrap/FeatureContext.php @@ -0,0 +1,73 @@ +" euro + Quand je sélectionne "" depuis "Operation" + Et je remplis "Montant" avec "" + Et je presse "Go" + Alors je devrais voir "Vous avez euro sur votre compte" + + Exemples: + | operation | montantInitial| montant | montantFinal | + | Ajouter | 50 | 10 | 60 | + | Ajouter | 50 | 20 | 70 | + | Ajouter | 50 | 5 | 55 | + | Ajouter | 50 | 0 | 50 | + | Retirer | 50 | 10 | 40 | + | Retirer | 50 | 20 | 30 | + | Retirer | 50 | 30 | 20 | + + Scénario: Les découverts sont interdits + Etant donné que j'ai "50" euro + Quand je sélectionne "Retirer" depuis "Operation" + Et je remplis "Montant" avec "60" + Et je presse "Go" + Alors je devrais voir "Vous avez 50 euro sur votre compte" + Et je devrais voir "Les decouverts ne sont pas autorises" \ No newline at end of file diff --git a/data/doc-prince-book-generation/resources/exercices/exo3/features/bootstrap/FeatureContext.php b/data/doc-prince-book-generation/resources/exercices/exo3/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..9b66852 --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo3/features/bootstrap/FeatureContext.php @@ -0,0 +1,49 @@ +_balance; + } + + /** + * Set balance + * + * @param float $balance + */ + public function setBalance($balance) { + $this->_balance = (float) $balance; + } + + /** + * Take money on the account + * + * @param float $amount + * @return \MyApp\Account + * @throws \Exception + */ + public function takeMoney($amount) { + if ($this->getBalance() - $amount <= 0) { + throw new \Exception("Les decouverts ne sont pas autorises"); + } + $this->_balance -= $amount; + return $this; + } + + /** + * Add money to the account + * + * @param float $amount + * @return \MyApp\Account + */ + public function addMoney($amount) { + $this->_balance += $amount; + } + +} \ No newline at end of file diff --git a/data/doc-prince-book-generation/resources/exercices/exo3/site/web/css/bootstrap.css b/data/doc-prince-book-generation/resources/exercices/exo3/site/web/css/bootstrap.css new file mode 100644 index 0000000..495188a --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo3/site/web/css/bootstrap.css @@ -0,0 +1,3990 @@ +/*! + * Bootstrap v2.0.2 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} +audio:not([controls]) { + display: none; +} +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +a:hover, +a:active { + outline: 0; +} +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + height: auto; + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; +} +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} +button, +input { + *overflow: visible; + line-height: normal; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} +input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} +textarea { + overflow: auto; + vertical-align: top; +} +.clearfix { + *zoom: 1; +} +.clearfix:before, +.clearfix:after { + display: table; + content: ""; +} +.clearfix:after { + clear: both; +} +.hide-text { + overflow: hidden; + text-indent: 100%; + white-space: nowrap; +} +.input-block-level { + display: block; + width: 100%; + min-height: 28px; + /* Make inputs at least the height of their button counterpart */ + + /* Makes inputs behave like true block-level elements */ + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; +} +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #333333; + background-color: #ffffff; +} +a { + color: #0088cc; + text-decoration: none; +} +a:hover { + color: #005580; + text-decoration: underline; +} +.row { + margin-left: -20px; + *zoom: 1; +} +.row:before, +.row:after { + display: table; + content: ""; +} +.row:after { + clear: both; +} +[class*="span"] { + float: left; + margin-left: 20px; +} +.container, +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} +.span12 { + width: 940px; +} +.span11 { + width: 860px; +} +.span10 { + width: 780px; +} +.span9 { + width: 700px; +} +.span8 { + width: 620px; +} +.span7 { + width: 540px; +} +.span6 { + width: 460px; +} +.span5 { + width: 380px; +} +.span4 { + width: 300px; +} +.span3 { + width: 220px; +} +.span2 { + width: 140px; +} +.span1 { + width: 60px; +} +.offset12 { + margin-left: 980px; +} +.offset11 { + margin-left: 900px; +} +.offset10 { + margin-left: 820px; +} +.offset9 { + margin-left: 740px; +} +.offset8 { + margin-left: 660px; +} +.offset7 { + margin-left: 580px; +} +.offset6 { + margin-left: 500px; +} +.offset5 { + margin-left: 420px; +} +.offset4 { + margin-left: 340px; +} +.offset3 { + margin-left: 260px; +} +.offset2 { + margin-left: 180px; +} +.offset1 { + margin-left: 100px; +} +.row-fluid { + width: 100%; + *zoom: 1; +} +.row-fluid:before, +.row-fluid:after { + display: table; + content: ""; +} +.row-fluid:after { + clear: both; +} +.row-fluid > [class*="span"] { + float: left; + margin-left: 2.127659574%; +} +.row-fluid > [class*="span"]:first-child { + margin-left: 0; +} +.row-fluid > .span12 { + width: 99.99999998999999%; +} +.row-fluid > .span11 { + width: 91.489361693%; +} +.row-fluid > .span10 { + width: 82.97872339599999%; +} +.row-fluid > .span9 { + width: 74.468085099%; +} +.row-fluid > .span8 { + width: 65.95744680199999%; +} +.row-fluid > .span7 { + width: 57.446808505%; +} +.row-fluid > .span6 { + width: 48.93617020799999%; +} +.row-fluid > .span5 { + width: 40.425531911%; +} +.row-fluid > .span4 { + width: 31.914893614%; +} +.row-fluid > .span3 { + width: 23.404255317%; +} +.row-fluid > .span2 { + width: 14.89361702%; +} +.row-fluid > .span1 { + width: 6.382978723%; +} +.container { + margin-left: auto; + margin-right: auto; + *zoom: 1; +} +.container:before, +.container:after { + display: table; + content: ""; +} +.container:after { + clear: both; +} +.container-fluid { + padding-left: 20px; + padding-right: 20px; + *zoom: 1; +} +.container-fluid:before, +.container-fluid:after { + display: table; + content: ""; +} +.container-fluid:after { + clear: both; +} +p { + margin: 0 0 9px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; +} +p small { + font-size: 11px; + color: #999999; +} +.lead { + margin-bottom: 18px; + font-size: 20px; + font-weight: 200; + line-height: 27px; +} +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + font-family: inherit; + font-weight: bold; + color: inherit; + text-rendering: optimizelegibility; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + color: #999999; +} +h1 { + font-size: 30px; + line-height: 36px; +} +h1 small { + font-size: 18px; +} +h2 { + font-size: 24px; + line-height: 36px; +} +h2 small { + font-size: 18px; +} +h3 { + line-height: 27px; + font-size: 18px; +} +h3 small { + font-size: 14px; +} +h4, +h5, +h6 { + line-height: 18px; +} +h4 { + font-size: 14px; +} +h4 small { + font-size: 12px; +} +h5 { + font-size: 12px; +} +h6 { + font-size: 11px; + color: #999999; + text-transform: uppercase; +} +.page-header { + padding-bottom: 17px; + margin: 18px 0; + border-bottom: 1px solid #eeeeee; +} +.page-header h1 { + line-height: 1; +} +ul, +ol { + padding: 0; + margin: 0 0 9px 25px; +} +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} +ul { + list-style: disc; +} +ol { + list-style: decimal; +} +li { + line-height: 18px; +} +ul.unstyled, +ol.unstyled { + margin-left: 0; + list-style: none; +} +dl { + margin-bottom: 18px; +} +dt, +dd { + line-height: 18px; +} +dt { + font-weight: bold; + line-height: 17px; +} +dd { + margin-left: 9px; +} +.dl-horizontal dt { + float: left; + clear: left; + width: 120px; + text-align: right; +} +.dl-horizontal dd { + margin-left: 130px; +} +hr { + margin: 18px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} +strong { + font-weight: bold; +} +em { + font-style: italic; +} +.muted { + color: #999999; +} +abbr[title] { + border-bottom: 1px dotted #ddd; + cursor: help; +} +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 0 0 0 15px; + margin: 0 0 18px; + border-left: 5px solid #eeeeee; +} +blockquote p { + margin-bottom: 0; + font-size: 16px; + font-weight: 300; + line-height: 22.5px; +} +blockquote small { + display: block; + line-height: 18px; + color: #999999; +} +blockquote small:before { + content: '\2014 \00A0'; +} +blockquote.pull-right { + float: right; + padding-left: 0; + padding-right: 15px; + border-left: 0; + border-right: 5px solid #eeeeee; +} +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} +address { + display: block; + margin-bottom: 18px; + line-height: 18px; + font-style: normal; +} +small { + font-size: 100%; +} +cite { + font-style: normal; +} +code, +pre { + padding: 0 3px 2px; + font-family: Menlo, Monaco, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +code { + padding: 2px 4px; + color: #d14; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} +pre { + display: block; + padding: 8.5px; + margin: 0 0 9px; + font-size: 12.025px; + line-height: 18px; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + white-space: pre; + white-space: pre-wrap; + word-break: break-all; + word-wrap: break-word; +} +pre.prettyprint { + margin-bottom: 18px; +} +pre code { + padding: 0; + color: inherit; + background-color: transparent; + border: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +form { + margin: 0 0 18px; +} +fieldset { + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 27px; + font-size: 19.5px; + line-height: 36px; + color: #333333; + border: 0; + border-bottom: 1px solid #eee; +} +legend small { + font-size: 13.5px; + color: #999999; +} +label, +input, +button, +select, +textarea { + font-size: 13px; + font-weight: normal; + line-height: 18px; +} +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} +label { + display: block; + margin-bottom: 5px; + color: #333333; +} +input, +textarea, +select, +.uneditable-input { + display: inline-block; + width: 210px; + height: 18px; + padding: 4px; + margin-bottom: 9px; + font-size: 13px; + line-height: 18px; + color: #555555; + border: 1px solid #cccccc; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.uneditable-textarea { + width: auto; + height: auto; +} +label input, +label textarea, +label select { + display: block; +} +input[type="image"], +input[type="checkbox"], +input[type="radio"] { + width: auto; + height: auto; + padding: 0; + margin: 3px 0; + *margin-top: 0; + /* IE7 */ + + line-height: normal; + cursor: pointer; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + border: 0 \9; + /* IE9 and down */ + +} +input[type="image"] { + border: 0; +} +input[type="file"] { + width: auto; + padding: initial; + line-height: initial; + border: initial; + background-color: #ffffff; + background-color: initial; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +input[type="button"], +input[type="reset"], +input[type="submit"] { + width: auto; + height: auto; +} +select, +input[type="file"] { + height: 28px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 28px; +} +input[type="file"] { + line-height: 18px \9; +} +select { + width: 220px; + background-color: #ffffff; +} +select[multiple], +select[size] { + height: auto; +} +input[type="image"] { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +textarea { + height: auto; +} +input[type="hidden"] { + display: none; +} +.radio, +.checkbox { + padding-left: 18px; +} +.radio input[type="radio"], +.checkbox input[type="checkbox"] { + float: left; + margin-left: -18px; +} +.controls > .radio:first-child, +.controls > .checkbox:first-child { + padding-top: 5px; +} +.radio.inline, +.checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} +.radio.inline + .radio.inline, +.checkbox.inline + .checkbox.inline { + margin-left: 10px; +} +input, +textarea { + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -ms-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} +input:focus, +textarea:focus { + border-color: rgba(82, 168, 236, 0.8); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus, +select:focus { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.input-mini { + width: 60px; +} +.input-small { + width: 90px; +} +.input-medium { + width: 150px; +} +.input-large { + width: 210px; +} +.input-xlarge { + width: 270px; +} +.input-xxlarge { + width: 530px; +} +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input { + float: none; + margin-left: 0; +} +input, +textarea, +.uneditable-input { + margin-left: 0; +} +input.span12, textarea.span12, .uneditable-input.span12 { + width: 930px; +} +input.span11, textarea.span11, .uneditable-input.span11 { + width: 850px; +} +input.span10, textarea.span10, .uneditable-input.span10 { + width: 770px; +} +input.span9, textarea.span9, .uneditable-input.span9 { + width: 690px; +} +input.span8, textarea.span8, .uneditable-input.span8 { + width: 610px; +} +input.span7, textarea.span7, .uneditable-input.span7 { + width: 530px; +} +input.span6, textarea.span6, .uneditable-input.span6 { + width: 450px; +} +input.span5, textarea.span5, .uneditable-input.span5 { + width: 370px; +} +input.span4, textarea.span4, .uneditable-input.span4 { + width: 290px; +} +input.span3, textarea.span3, .uneditable-input.span3 { + width: 210px; +} +input.span2, textarea.span2, .uneditable-input.span2 { + width: 130px; +} +input.span1, textarea.span1, .uneditable-input.span1 { + width: 50px; +} +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + background-color: #eeeeee; + border-color: #ddd; + cursor: not-allowed; +} +.control-group.warning > label, +.control-group.warning .help-block, +.control-group.warning .help-inline { + color: #c09853; +} +.control-group.warning input, +.control-group.warning select, +.control-group.warning textarea { + color: #c09853; + border-color: #c09853; +} +.control-group.warning input:focus, +.control-group.warning select:focus, +.control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: 0 0 6px #dbc59e; + -moz-box-shadow: 0 0 6px #dbc59e; + box-shadow: 0 0 6px #dbc59e; +} +.control-group.warning .input-prepend .add-on, +.control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} +.control-group.error > label, +.control-group.error .help-block, +.control-group.error .help-inline { + color: #b94a48; +} +.control-group.error input, +.control-group.error select, +.control-group.error textarea { + color: #b94a48; + border-color: #b94a48; +} +.control-group.error input:focus, +.control-group.error select:focus, +.control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: 0 0 6px #d59392; + -moz-box-shadow: 0 0 6px #d59392; + box-shadow: 0 0 6px #d59392; +} +.control-group.error .input-prepend .add-on, +.control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} +.control-group.success > label, +.control-group.success .help-block, +.control-group.success .help-inline { + color: #468847; +} +.control-group.success input, +.control-group.success select, +.control-group.success textarea { + color: #468847; + border-color: #468847; +} +.control-group.success input:focus, +.control-group.success select:focus, +.control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: 0 0 6px #7aba7b; + -moz-box-shadow: 0 0 6px #7aba7b; + box-shadow: 0 0 6px #7aba7b; +} +.control-group.success .input-prepend .add-on, +.control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} +input:focus:required:invalid, +textarea:focus:required:invalid, +select:focus:required:invalid { + color: #b94a48; + border-color: #ee5f5b; +} +input:focus:required:invalid:focus, +textarea:focus:required:invalid:focus, +select:focus:required:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} +.form-actions { + padding: 17px 20px 18px; + margin-top: 18px; + margin-bottom: 18px; + background-color: #eeeeee; + border-top: 1px solid #ddd; + *zoom: 1; +} +.form-actions:before, +.form-actions:after { + display: table; + content: ""; +} +.form-actions:after { + clear: both; +} +.uneditable-input { + display: block; + background-color: #ffffff; + border-color: #eee; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + cursor: not-allowed; +} +:-moz-placeholder { + color: #999999; +} +::-webkit-input-placeholder { + color: #999999; +} +.help-block, +.help-inline { + color: #555555; +} +.help-block { + display: block; + margin-bottom: 9px; +} +.help-inline { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + vertical-align: middle; + padding-left: 5px; +} +.input-prepend, +.input-append { + margin-bottom: 5px; +} +.input-prepend input, +.input-append input, +.input-prepend select, +.input-append select, +.input-prepend .uneditable-input, +.input-append .uneditable-input { + *margin-left: 0; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-prepend input:focus, +.input-append input:focus, +.input-prepend select:focus, +.input-append select:focus, +.input-prepend .uneditable-input:focus, +.input-append .uneditable-input:focus { + position: relative; + z-index: 2; +} +.input-prepend .uneditable-input, +.input-append .uneditable-input { + border-left-color: #ccc; +} +.input-prepend .add-on, +.input-append .add-on { + display: inline-block; + width: auto; + min-width: 16px; + height: 18px; + padding: 4px 5px; + font-weight: normal; + line-height: 18px; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + vertical-align: middle; + background-color: #eeeeee; + border: 1px solid #ccc; +} +.input-prepend .add-on, +.input-append .add-on, +.input-prepend .btn, +.input-append .btn { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-prepend .active, +.input-append .active { + background-color: #a9dba9; + border-color: #46a546; +} +.input-prepend .add-on, +.input-prepend .btn { + margin-right: -1px; +} +.input-append input, +.input-append select .uneditable-input { + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-append .uneditable-input { + border-left-color: #eee; + border-right-color: #ccc; +} +.input-append .add-on, +.input-append .btn { + margin-left: -1px; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-prepend.input-append input, +.input-prepend.input-append select, +.input-prepend.input-append .uneditable-input { + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.input-prepend.input-append .add-on:first-child, +.input-prepend.input-append .btn:first-child { + margin-right: -1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-prepend.input-append .add-on:last-child, +.input-prepend.input-append .btn:last-child { + margin-left: -1px; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.search-query { + padding-left: 14px; + padding-right: 14px; + margin-bottom: 0; + -webkit-border-radius: 14px; + -moz-border-radius: 14px; + border-radius: 14px; +} +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input, +.form-search .input-prepend, +.form-inline .input-prepend, +.form-horizontal .input-prepend, +.form-search .input-append, +.form-inline .input-append, +.form-horizontal .input-append { + display: inline-block; + margin-bottom: 0; +} +.form-search .hide, +.form-inline .hide, +.form-horizontal .hide { + display: none; +} +.form-search label, +.form-inline label { + display: inline-block; +} +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + margin-bottom: 0; +} +.form-search .radio, +.form-search .checkbox, +.form-inline .radio, +.form-inline .checkbox { + padding-left: 0; + margin-bottom: 0; + vertical-align: middle; +} +.form-search .radio input[type="radio"], +.form-search .checkbox input[type="checkbox"], +.form-inline .radio input[type="radio"], +.form-inline .checkbox input[type="checkbox"] { + float: left; + margin-left: 0; + margin-right: 3px; +} +.control-group { + margin-bottom: 9px; +} +legend + .control-group { + margin-top: 18px; + -webkit-margin-top-collapse: separate; +} +.form-horizontal .control-group { + margin-bottom: 18px; + *zoom: 1; +} +.form-horizontal .control-group:before, +.form-horizontal .control-group:after { + display: table; + content: ""; +} +.form-horizontal .control-group:after { + clear: both; +} +.form-horizontal .control-label { + float: left; + width: 140px; + padding-top: 5px; + text-align: right; +} +.form-horizontal .controls { + margin-left: 160px; + /* Super jank IE7 fix to ensure the inputs in .input-append and input-prepend don't inherit the margin of the parent, in this case .controls */ + + *display: inline-block; + *margin-left: 0; + *padding-left: 20px; +} +.form-horizontal .help-block { + margin-top: 9px; + margin-bottom: 0; +} +.form-horizontal .form-actions { + padding-left: 160px; +} +table { + max-width: 100%; + border-collapse: collapse; + border-spacing: 0; + background-color: transparent; +} +.table { + width: 100%; + margin-bottom: 18px; +} +.table th, +.table td { + padding: 8px; + line-height: 18px; + text-align: left; + vertical-align: top; + border-top: 1px solid #dddddd; +} +.table th { + font-weight: bold; +} +.table thead th { + vertical-align: bottom; +} +.table colgroup + thead tr:first-child th, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child th, +.table thead:first-child tr:first-child td { + border-top: 0; +} +.table tbody + tbody { + border-top: 2px solid #dddddd; +} +.table-condensed th, +.table-condensed td { + padding: 4px 5px; +} +.table-bordered { + border: 1px solid #dddddd; + border-left: 0; + border-collapse: separate; + *border-collapse: collapsed; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.table-bordered th, +.table-bordered td { + border-left: 1px solid #dddddd; +} +.table-bordered thead:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child th, +.table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} +.table-bordered thead:first-child tr:first-child th:first-child, +.table-bordered tbody:first-child tr:first-child td:first-child { + -webkit-border-radius: 4px 0 0 0; + -moz-border-radius: 4px 0 0 0; + border-radius: 4px 0 0 0; +} +.table-bordered thead:first-child tr:first-child th:last-child, +.table-bordered tbody:first-child tr:first-child td:last-child { + -webkit-border-radius: 0 4px 0 0; + -moz-border-radius: 0 4px 0 0; + border-radius: 0 4px 0 0; +} +.table-bordered thead:last-child tr:last-child th:first-child, +.table-bordered tbody:last-child tr:last-child td:first-child { + -webkit-border-radius: 0 0 0 4px; + -moz-border-radius: 0 0 0 4px; + border-radius: 0 0 0 4px; +} +.table-bordered thead:last-child tr:last-child th:last-child, +.table-bordered tbody:last-child tr:last-child td:last-child { + -webkit-border-radius: 0 0 4px 0; + -moz-border-radius: 0 0 4px 0; + border-radius: 0 0 4px 0; +} +.table-striped tbody tr:nth-child(odd) td, +.table-striped tbody tr:nth-child(odd) th { + background-color: #f9f9f9; +} +.table tbody tr:hover td, +.table tbody tr:hover th { + background-color: #f5f5f5; +} +table .span1 { + float: none; + width: 44px; + margin-left: 0; +} +table .span2 { + float: none; + width: 124px; + margin-left: 0; +} +table .span3 { + float: none; + width: 204px; + margin-left: 0; +} +table .span4 { + float: none; + width: 284px; + margin-left: 0; +} +table .span5 { + float: none; + width: 364px; + margin-left: 0; +} +table .span6 { + float: none; + width: 444px; + margin-left: 0; +} +table .span7 { + float: none; + width: 524px; + margin-left: 0; +} +table .span8 { + float: none; + width: 604px; + margin-left: 0; +} +table .span9 { + float: none; + width: 684px; + margin-left: 0; +} +table .span10 { + float: none; + width: 764px; + margin-left: 0; +} +table .span11 { + float: none; + width: 844px; + margin-left: 0; +} +table .span12 { + float: none; + width: 924px; + margin-left: 0; +} +table .span13 { + float: none; + width: 1004px; + margin-left: 0; +} +table .span14 { + float: none; + width: 1084px; + margin-left: 0; +} +table .span15 { + float: none; + width: 1164px; + margin-left: 0; +} +table .span16 { + float: none; + width: 1244px; + margin-left: 0; +} +table .span17 { + float: none; + width: 1324px; + margin-left: 0; +} +table .span18 { + float: none; + width: 1404px; + margin-left: 0; +} +table .span19 { + float: none; + width: 1484px; + margin-left: 0; +} +table .span20 { + float: none; + width: 1564px; + margin-left: 0; +} +table .span21 { + float: none; + width: 1644px; + margin-left: 0; +} +table .span22 { + float: none; + width: 1724px; + margin-left: 0; +} +table .span23 { + float: none; + width: 1804px; + margin-left: 0; +} +table .span24 { + float: none; + width: 1884px; + margin-left: 0; +} +[class^="icon-"], +[class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; + *margin-right: .3em; +} +[class^="icon-"]:last-child, +[class*=" icon-"]:last-child { + *margin-left: 0; +} +.icon-white { + background-image: url("../img/glyphicons-halflings-white.png"); +} +.icon-glass { + background-position: 0 0; +} +.icon-music { + background-position: -24px 0; +} +.icon-search { + background-position: -48px 0; +} +.icon-envelope { + background-position: -72px 0; +} +.icon-heart { + background-position: -96px 0; +} +.icon-star { + background-position: -120px 0; +} +.icon-star-empty { + background-position: -144px 0; +} +.icon-user { + background-position: -168px 0; +} +.icon-film { + background-position: -192px 0; +} +.icon-th-large { + background-position: -216px 0; +} +.icon-th { + background-position: -240px 0; +} +.icon-th-list { + background-position: -264px 0; +} +.icon-ok { + background-position: -288px 0; +} +.icon-remove { + background-position: -312px 0; +} +.icon-zoom-in { + background-position: -336px 0; +} +.icon-zoom-out { + background-position: -360px 0; +} +.icon-off { + background-position: -384px 0; +} +.icon-signal { + background-position: -408px 0; +} +.icon-cog { + background-position: -432px 0; +} +.icon-trash { + background-position: -456px 0; +} +.icon-home { + background-position: 0 -24px; +} +.icon-file { + background-position: -24px -24px; +} +.icon-time { + background-position: -48px -24px; +} +.icon-road { + background-position: -72px -24px; +} +.icon-download-alt { + background-position: -96px -24px; +} +.icon-download { + background-position: -120px -24px; +} +.icon-upload { + background-position: -144px -24px; +} +.icon-inbox { + background-position: -168px -24px; +} +.icon-play-circle { + background-position: -192px -24px; +} +.icon-repeat { + background-position: -216px -24px; +} +.icon-refresh { + background-position: -240px -24px; +} +.icon-list-alt { + background-position: -264px -24px; +} +.icon-lock { + background-position: -287px -24px; +} +.icon-flag { + background-position: -312px -24px; +} +.icon-headphones { + background-position: -336px -24px; +} +.icon-volume-off { + background-position: -360px -24px; +} +.icon-volume-down { + background-position: -384px -24px; +} +.icon-volume-up { + background-position: -408px -24px; +} +.icon-qrcode { + background-position: -432px -24px; +} +.icon-barcode { + background-position: -456px -24px; +} +.icon-tag { + background-position: 0 -48px; +} +.icon-tags { + background-position: -25px -48px; +} +.icon-book { + background-position: -48px -48px; +} +.icon-bookmark { + background-position: -72px -48px; +} +.icon-print { + background-position: -96px -48px; +} +.icon-camera { + background-position: -120px -48px; +} +.icon-font { + background-position: -144px -48px; +} +.icon-bold { + background-position: -167px -48px; +} +.icon-italic { + background-position: -192px -48px; +} +.icon-text-height { + background-position: -216px -48px; +} +.icon-text-width { + background-position: -240px -48px; +} +.icon-align-left { + background-position: -264px -48px; +} +.icon-align-center { + background-position: -288px -48px; +} +.icon-align-right { + background-position: -312px -48px; +} +.icon-align-justify { + background-position: -336px -48px; +} +.icon-list { + background-position: -360px -48px; +} +.icon-indent-left { + background-position: -384px -48px; +} +.icon-indent-right { + background-position: -408px -48px; +} +.icon-facetime-video { + background-position: -432px -48px; +} +.icon-picture { + background-position: -456px -48px; +} +.icon-pencil { + background-position: 0 -72px; +} +.icon-map-marker { + background-position: -24px -72px; +} +.icon-adjust { + background-position: -48px -72px; +} +.icon-tint { + background-position: -72px -72px; +} +.icon-edit { + background-position: -96px -72px; +} +.icon-share { + background-position: -120px -72px; +} +.icon-check { + background-position: -144px -72px; +} +.icon-move { + background-position: -168px -72px; +} +.icon-step-backward { + background-position: -192px -72px; +} +.icon-fast-backward { + background-position: -216px -72px; +} +.icon-backward { + background-position: -240px -72px; +} +.icon-play { + background-position: -264px -72px; +} +.icon-pause { + background-position: -288px -72px; +} +.icon-stop { + background-position: -312px -72px; +} +.icon-forward { + background-position: -336px -72px; +} +.icon-fast-forward { + background-position: -360px -72px; +} +.icon-step-forward { + background-position: -384px -72px; +} +.icon-eject { + background-position: -408px -72px; +} +.icon-chevron-left { + background-position: -432px -72px; +} +.icon-chevron-right { + background-position: -456px -72px; +} +.icon-plus-sign { + background-position: 0 -96px; +} +.icon-minus-sign { + background-position: -24px -96px; +} +.icon-remove-sign { + background-position: -48px -96px; +} +.icon-ok-sign { + background-position: -72px -96px; +} +.icon-question-sign { + background-position: -96px -96px; +} +.icon-info-sign { + background-position: -120px -96px; +} +.icon-screenshot { + background-position: -144px -96px; +} +.icon-remove-circle { + background-position: -168px -96px; +} +.icon-ok-circle { + background-position: -192px -96px; +} +.icon-ban-circle { + background-position: -216px -96px; +} +.icon-arrow-left { + background-position: -240px -96px; +} +.icon-arrow-right { + background-position: -264px -96px; +} +.icon-arrow-up { + background-position: -289px -96px; +} +.icon-arrow-down { + background-position: -312px -96px; +} +.icon-share-alt { + background-position: -336px -96px; +} +.icon-resize-full { + background-position: -360px -96px; +} +.icon-resize-small { + background-position: -384px -96px; +} +.icon-plus { + background-position: -408px -96px; +} +.icon-minus { + background-position: -433px -96px; +} +.icon-asterisk { + background-position: -456px -96px; +} +.icon-exclamation-sign { + background-position: 0 -120px; +} +.icon-gift { + background-position: -24px -120px; +} +.icon-leaf { + background-position: -48px -120px; +} +.icon-fire { + background-position: -72px -120px; +} +.icon-eye-open { + background-position: -96px -120px; +} +.icon-eye-close { + background-position: -120px -120px; +} +.icon-warning-sign { + background-position: -144px -120px; +} +.icon-plane { + background-position: -168px -120px; +} +.icon-calendar { + background-position: -192px -120px; +} +.icon-random { + background-position: -216px -120px; +} +.icon-comment { + background-position: -240px -120px; +} +.icon-magnet { + background-position: -264px -120px; +} +.icon-chevron-up { + background-position: -288px -120px; +} +.icon-chevron-down { + background-position: -313px -119px; +} +.icon-retweet { + background-position: -336px -120px; +} +.icon-shopping-cart { + background-position: -360px -120px; +} +.icon-folder-close { + background-position: -384px -120px; +} +.icon-folder-open { + background-position: -408px -120px; +} +.icon-resize-vertical { + background-position: -432px -119px; +} +.icon-resize-horizontal { + background-position: -456px -118px; +} +.dropdown { + position: relative; +} +.dropdown-toggle { + *margin-bottom: -3px; +} +.dropdown-toggle:active, +.open .dropdown-toggle { + outline: 0; +} +.caret { + display: inline-block; + width: 0; + height: 0; + vertical-align: top; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #000000; + opacity: 0.3; + filter: alpha(opacity=30); + content: ""; +} +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} +.dropdown:hover .caret, +.open.dropdown .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + float: left; + display: none; + min-width: 160px; + padding: 4px 0; + margin: 0; + list-style: none; + background-color: #ffffff; + border-color: #ccc; + border-color: rgba(0, 0, 0, 0.2); + border-style: solid; + border-width: 1px; + -webkit-border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + border-radius: 0 0 5px 5px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; + *border-right-width: 2px; + *border-bottom-width: 2px; +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 8px 1px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; + *width: 100%; + *margin: -5px 0 5px; +} +.dropdown-menu a { + display: block; + padding: 3px 15px; + clear: both; + font-weight: normal; + line-height: 18px; + color: #333333; + white-space: nowrap; +} +.dropdown-menu li > a:hover, +.dropdown-menu .active > a, +.dropdown-menu .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; +} +.dropdown.open { + *z-index: 1000; +} +.dropdown.open .dropdown-toggle { + color: #ffffff; + background: #ccc; + background: rgba(0, 0, 0, 0.3); +} +.dropdown.open .dropdown-menu { + display: block; +} +.pull-right .dropdown-menu { + left: auto; + right: 0; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0; + border-bottom: 4px solid #000000; + content: "\2191"; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} +.typeahead { + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #eee; + border: 1px solid rgba(0, 0, 0, 0.05); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} +.well-large { + padding: 24px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} +.well-small { + padding: 9px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.fade { + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -ms-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; + opacity: 0; +} +.fade.in { + opacity: 1; +} +.collapse { + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -ms-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; + position: relative; + overflow: hidden; + height: 0; +} +.collapse.in { + height: auto; +} +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 18px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} +.close:hover { + color: #000000; + text-decoration: none; + opacity: 0.4; + filter: alpha(opacity=40); + cursor: pointer; +} +.btn { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + padding: 4px 10px 4px; + margin-bottom: 0; + font-size: 13px; + line-height: 18px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + background-color: #f5f5f5; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(top, #ffffff, #e6e6e6); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + border: 1px solid #cccccc; + border-bottom-color: #b3b3b3; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + cursor: pointer; + *margin-left: .3em; +} +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + background-color: #e6e6e6; +} +.btn:active, +.btn.active { + background-color: #cccccc \9; +} +.btn:first-child { + *margin-left: 0; +} +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -ms-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn.active, +.btn:active { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + outline: 0; +} +.btn.disabled, +.btn[disabled] { + cursor: default; + background-image: none; + background-color: #e6e6e6; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +.btn-large { + padding: 9px 14px; + font-size: 15px; + line-height: normal; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-large [class^="icon-"] { + margin-top: 1px; +} +.btn-small { + padding: 5px 9px; + font-size: 11px; + line-height: 16px; +} +.btn-small [class^="icon-"] { + margin-top: -1px; +} +.btn-mini { + padding: 2px 6px; + font-size: 11px; + line-height: 14px; +} +.btn-primary, +.btn-primary:hover, +.btn-warning, +.btn-warning:hover, +.btn-danger, +.btn-danger:hover, +.btn-success, +.btn-success:hover, +.btn-info, +.btn-info:hover, +.btn-inverse, +.btn-inverse:hover { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + color: #ffffff; +} +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-inverse.active { + color: rgba(255, 255, 255, 0.75); +} +.btn-primary { + background-color: #0074cc; + background-image: -moz-linear-gradient(top, #0088cc, #0055cc); + background-image: -ms-linear-gradient(top, #0088cc, #0055cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0055cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0055cc); + background-image: -o-linear-gradient(top, #0088cc, #0055cc); + background-image: linear-gradient(top, #0088cc, #0055cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0055cc', GradientType=0); + border-color: #0055cc #0055cc #003580; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + background-color: #0055cc; +} +.btn-primary:active, +.btn-primary.active { + background-color: #004099 \9; +} +.btn-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -ms-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + background-color: #f89406; +} +.btn-warning:active, +.btn-warning.active { + background-color: #c67605 \9; +} +.btn-danger { + background-color: #da4f49; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(top, #ee5f5b, #bd362f); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + background-color: #bd362f; +} +.btn-danger:active, +.btn-danger.active { + background-color: #942a25 \9; +} +.btn-success { + background-color: #5bb75b; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -ms-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(top, #62c462, #51a351); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + background-color: #51a351; +} +.btn-success:active, +.btn-success.active { + background-color: #408140 \9; +} +.btn-info { + background-color: #49afcd; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(top, #5bc0de, #2f96b4); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + background-color: #2f96b4; +} +.btn-info:active, +.btn-info.active { + background-color: #24748c \9; +} +.btn-inverse { + background-color: #414141; + background-image: -moz-linear-gradient(top, #555555, #222222); + background-image: -ms-linear-gradient(top, #555555, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222)); + background-image: -webkit-linear-gradient(top, #555555, #222222); + background-image: -o-linear-gradient(top, #555555, #222222); + background-image: linear-gradient(top, #555555, #222222); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0); + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); +} +.btn-inverse:hover, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + background-color: #222222; +} +.btn-inverse:active, +.btn-inverse.active { + background-color: #080808 \9; +} +button.btn, +input[type="submit"].btn { + *padding-top: 2px; + *padding-bottom: 2px; +} +button.btn::-moz-focus-inner, +input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} +button.btn.btn-large, +input[type="submit"].btn.btn-large { + *padding-top: 7px; + *padding-bottom: 7px; +} +button.btn.btn-small, +input[type="submit"].btn.btn-small { + *padding-top: 3px; + *padding-bottom: 3px; +} +button.btn.btn-mini, +input[type="submit"].btn.btn-mini { + *padding-top: 1px; + *padding-bottom: 1px; +} +.btn-group { + position: relative; + *zoom: 1; + *margin-left: .3em; +} +.btn-group:before, +.btn-group:after { + display: table; + content: ""; +} +.btn-group:after { + clear: both; +} +.btn-group:first-child { + *margin-left: 0; +} +.btn-group + .btn-group { + margin-left: 5px; +} +.btn-toolbar { + margin-top: 9px; + margin-bottom: 9px; +} +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} +.btn-group .btn { + position: relative; + float: left; + margin-left: -1px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.btn-group .btn:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; + border-top-left-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + border-bottom-left-radius: 4px; +} +.btn-group .btn:last-child, +.btn-group .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; + border-bottom-right-radius: 4px; +} +.btn-group .btn.large:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 6px; + -moz-border-radius-topleft: 6px; + border-top-left-radius: 6px; + -webkit-border-bottom-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + border-bottom-left-radius: 6px; +} +.btn-group .btn.large:last-child, +.btn-group .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + -moz-border-radius-topright: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + -moz-border-radius-bottomright: 6px; + border-bottom-right-radius: 6px; +} +.btn-group .btn:hover, +.btn-group .btn:focus, +.btn-group .btn:active, +.btn-group .btn.active { + z-index: 2; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + *padding-top: 3px; + *padding-bottom: 3px; +} +.btn-group .btn-mini.dropdown-toggle { + padding-left: 5px; + padding-right: 5px; + *padding-top: 1px; + *padding-bottom: 1px; +} +.btn-group .btn-small.dropdown-toggle { + *padding-top: 4px; + *padding-bottom: 4px; +} +.btn-group .btn-large.dropdown-toggle { + padding-left: 12px; + padding-right: 12px; +} +.btn-group.open { + *z-index: 1000; +} +.btn-group.open .dropdown-menu { + display: block; + margin-top: 1px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.btn .caret { + margin-top: 7px; + margin-left: 0; +} +.btn:hover .caret, +.open.btn-group .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.btn-mini .caret { + margin-top: 5px; +} +.btn-small .caret { + margin-top: 6px; +} +.btn-large .caret { + margin-top: 6px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.btn-primary .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 0.75; + filter: alpha(opacity=75); +} +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 18px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + color: #c09853; +} +.alert-heading { + color: inherit; +} +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 18px; +} +.alert-success { + background-color: #dff0d8; + border-color: #d6e9c6; + color: #468847; +} +.alert-danger, +.alert-error { + background-color: #f2dede; + border-color: #eed3d7; + color: #b94a48; +} +.alert-info { + background-color: #d9edf7; + border-color: #bce8f1; + color: #3a87ad; +} +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} +.alert-block > p, +.alert-block > ul { + margin-bottom: 0; +} +.alert-block p + p { + margin-top: 5px; +} +.nav { + margin-left: 0; + margin-bottom: 18px; + list-style: none; +} +.nav > li > a { + display: block; +} +.nav > li > a:hover { + text-decoration: none; + background-color: #eeeeee; +} +.nav .nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 18px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} +.nav li + .nav-header { + margin-top: 9px; +} +.nav-list { + padding-left: 15px; + padding-right: 15px; + margin-bottom: 0; +} +.nav-list > li > a, +.nav-list .nav-header { + margin-left: -15px; + margin-right: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} +.nav-list > li > a { + padding: 3px 15px; +} +.nav-list > .active > a, +.nav-list > .active > a:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} +.nav-list [class^="icon-"] { + margin-right: 2px; +} +.nav-list .divider { + height: 1px; + margin: 8px 1px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; + *width: 100%; + *margin: -5px 0 5px; +} +.nav-tabs, +.nav-pills { + *zoom: 1; +} +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + content: ""; +} +.nav-tabs:after, +.nav-pills:after { + clear: both; +} +.nav-tabs > li, +.nav-pills > li { + float: left; +} +.nav-tabs > li > a, +.nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + margin-bottom: -1px; +} +.nav-tabs > li > a { + padding-top: 8px; + padding-bottom: 8px; + line-height: 18px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} +.nav-tabs > .active > a, +.nav-tabs > .active > a:hover { + color: #555555; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; + cursor: default; +} +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.nav-pills > .active > a, +.nav-pills > .active > a:hover { + color: #ffffff; + background-color: #0088cc; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li > a { + margin-right: 0; +} +.nav-tabs.nav-stacked { + border-bottom: 0; +} +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} +.nav-tabs.nav-stacked > li > a:hover { + border-color: #ddd; + z-index: 2; +} +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} +.nav-tabs .dropdown-menu, +.nav-pills .dropdown-menu { + margin-top: 1px; + border-width: 1px; +} +.nav-pills .dropdown-menu { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.nav-tabs .dropdown-toggle .caret, +.nav-pills .dropdown-toggle .caret { + border-top-color: #0088cc; + border-bottom-color: #0088cc; + margin-top: 6px; +} +.nav-tabs .dropdown-toggle:hover .caret, +.nav-pills .dropdown-toggle:hover .caret { + border-top-color: #005580; + border-bottom-color: #005580; +} +.nav-tabs .active .dropdown-toggle .caret, +.nav-pills .active .dropdown-toggle .caret { + border-top-color: #333333; + border-bottom-color: #333333; +} +.nav > .dropdown.active > a:hover { + color: #000000; + cursor: pointer; +} +.nav-tabs .open .dropdown-toggle, +.nav-pills .open .dropdown-toggle, +.nav > .open.active > a:hover { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} +.nav .open .caret, +.nav .open.active .caret, +.nav .open a:hover .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} +.tabs-stacked .open > a:hover { + border-color: #999999; +} +.tabbable { + *zoom: 1; +} +.tabbable:before, +.tabbable:after { + display: table; + content: ""; +} +.tabbable:after { + clear: both; +} +.tab-content { + display: table; + width: 100%; +} +.tabs-below .nav-tabs, +.tabs-right .nav-tabs, +.tabs-left .nav-tabs { + border-bottom: 0; +} +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} +.tab-content > .active, +.pill-content > .active { + display: block; +} +.tabs-below .nav-tabs { + border-top: 1px solid #ddd; +} +.tabs-below .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} +.tabs-below .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} +.tabs-below .nav-tabs > li > a:hover { + border-bottom-color: transparent; + border-top-color: #ddd; +} +.tabs-below .nav-tabs .active > a, +.tabs-below .nav-tabs .active > a:hover { + border-color: transparent #ddd #ddd #ddd; +} +.tabs-left .nav-tabs > li, +.tabs-right .nav-tabs > li { + float: none; +} +.tabs-left .nav-tabs > li > a, +.tabs-right .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} +.tabs-left .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} +.tabs-left .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} +.tabs-left .nav-tabs > li > a:hover { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} +.tabs-left .nav-tabs .active > a, +.tabs-left .nav-tabs .active > a:hover { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} +.tabs-right .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} +.tabs-right .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} +.tabs-right .nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} +.tabs-right .nav-tabs .active > a, +.tabs-right .nav-tabs .active > a:hover { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} +.navbar { + *position: relative; + *z-index: 2; + overflow: visible; + margin-bottom: 18px; +} +.navbar-inner { + padding-left: 20px; + padding-right: 20px; + background-color: #2c2c2c; + background-image: -moz-linear-gradient(top, #333333, #222222); + background-image: -ms-linear-gradient(top, #333333, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); + background-image: -webkit-linear-gradient(top, #333333, #222222); + background-image: -o-linear-gradient(top, #333333, #222222); + background-image: linear-gradient(top, #333333, #222222); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); +} +.navbar .container { + width: auto; +} +.btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-left: 5px; + margin-right: 5px; + background-color: #2c2c2c; + background-image: -moz-linear-gradient(top, #333333, #222222); + background-image: -ms-linear-gradient(top, #333333, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); + background-image: -webkit-linear-gradient(top, #333333, #222222); + background-image: -o-linear-gradient(top, #333333, #222222); + background-image: linear-gradient(top, #333333, #222222); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:dximagetransform.microsoft.gradient(enabled=false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} +.btn-navbar:hover, +.btn-navbar:active, +.btn-navbar.active, +.btn-navbar.disabled, +.btn-navbar[disabled] { + background-color: #222222; +} +.btn-navbar:active, +.btn-navbar.active { + background-color: #080808 \9; +} +.btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} +.nav-collapse.collapse { + height: auto; +} +.navbar { + color: #999999; +} +.navbar .brand:hover { + text-decoration: none; +} +.navbar .brand { + float: left; + display: block; + padding: 8px 20px 12px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + line-height: 1; + color: #ffffff; +} +.navbar .navbar-text { + margin-bottom: 0; + line-height: 40px; +} +.navbar .btn, +.navbar .btn-group { + margin-top: 5px; +} +.navbar .btn-group .btn { + margin-top: 0; +} +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} +.navbar-form:before, +.navbar-form:after { + display: table; + content: ""; +} +.navbar-form:after { + clear: both; +} +.navbar-form input, +.navbar-form select, +.navbar-form .radio, +.navbar-form .checkbox { + margin-top: 5px; +} +.navbar-form input, +.navbar-form select { + display: inline-block; + margin-bottom: 0; +} +.navbar-form input[type="image"], +.navbar-form input[type="checkbox"], +.navbar-form input[type="radio"] { + margin-top: 3px; +} +.navbar-form .input-append, +.navbar-form .input-prepend { + margin-top: 6px; + white-space: nowrap; +} +.navbar-form .input-append input, +.navbar-form .input-prepend input { + margin-top: 0; +} +.navbar-search { + position: relative; + float: left; + margin-top: 6px; + margin-bottom: 0; +} +.navbar-search .search-query { + padding: 4px 9px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + color: #ffffff; + background-color: #626262; + border: 1px solid #151515; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -ms-transition: none; + -o-transition: none; + transition: none; +} +.navbar-search .search-query:-moz-placeholder { + color: #cccccc; +} +.navbar-search .search-query::-webkit-input-placeholder { + color: #cccccc; +} +.navbar-search .search-query:focus, +.navbar-search .search-query.focused { + padding: 5px 10px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + outline: 0; +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; + margin-bottom: 0; +} +.navbar-fixed-top .navbar-inner, +.navbar-fixed-bottom .navbar-inner { + padding-left: 0; + padding-right: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.navbar-fixed-top .container, +.navbar-fixed-bottom .container { + width: 940px; +} +.navbar-fixed-top { + top: 0; +} +.navbar-fixed-bottom { + bottom: 0; +} +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} +.navbar .nav.pull-right { + float: right; +} +.navbar .nav > li { + display: block; + float: left; +} +.navbar .nav > li > a { + float: none; + padding: 10px 10px 11px; + line-height: 19px; + color: #999999; + text-decoration: none; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.navbar .nav > li > a:hover { + background-color: transparent; + color: #ffffff; + text-decoration: none; +} +.navbar .nav .active > a, +.navbar .nav .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #222222; +} +.navbar .divider-vertical { + height: 40px; + width: 1px; + margin: 0 9px; + overflow: hidden; + background-color: #222222; + border-right: 1px solid #333333; +} +.navbar .nav.pull-right { + margin-left: 10px; + margin-right: 0; +} +.navbar .dropdown-menu { + margin-top: 1px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.navbar .dropdown-menu:before { + content: ''; + display: inline-block; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + left: 9px; +} +.navbar .dropdown-menu:after { + content: ''; + display: inline-block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + left: 10px; +} +.navbar-fixed-bottom .dropdown-menu:before { + border-top: 7px solid #ccc; + border-top-color: rgba(0, 0, 0, 0.2); + border-bottom: 0; + bottom: -7px; + top: auto; +} +.navbar-fixed-bottom .dropdown-menu:after { + border-top: 6px solid #ffffff; + border-bottom: 0; + bottom: -6px; + top: auto; +} +.navbar .nav .dropdown-toggle .caret, +.navbar .nav .open.dropdown .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} +.navbar .nav .active .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.navbar .nav .open > .dropdown-toggle, +.navbar .nav .active > .dropdown-toggle, +.navbar .nav .open.active > .dropdown-toggle { + background-color: transparent; +} +.navbar .nav .active > .dropdown-toggle:hover { + color: #ffffff; +} +.navbar .nav.pull-right .dropdown-menu, +.navbar .nav .dropdown-menu.pull-right { + left: auto; + right: 0; +} +.navbar .nav.pull-right .dropdown-menu:before, +.navbar .nav .dropdown-menu.pull-right:before { + left: auto; + right: 12px; +} +.navbar .nav.pull-right .dropdown-menu:after, +.navbar .nav .dropdown-menu.pull-right:after { + left: auto; + right: 13px; +} +.breadcrumb { + padding: 7px 14px; + margin: 0 0 18px; + list-style: none; + background-color: #fbfbfb; + background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -ms-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5)); + background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); + background-image: linear-gradient(top, #ffffff, #f5f5f5); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0); + border: 1px solid #ddd; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} +.breadcrumb li { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + text-shadow: 0 1px 0 #ffffff; +} +.breadcrumb .divider { + padding: 0 5px; + color: #999999; +} +.breadcrumb .active a { + color: #333333; +} +.pagination { + height: 36px; + margin: 18px 0; +} +.pagination ul { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + margin-left: 0; + margin-bottom: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} +.pagination li { + display: inline; +} +.pagination a { + float: left; + padding: 0 14px; + line-height: 34px; + text-decoration: none; + border: 1px solid #ddd; + border-left-width: 0; +} +.pagination a:hover, +.pagination .active a { + background-color: #f5f5f5; +} +.pagination .active a { + color: #999999; + cursor: default; +} +.pagination .disabled span, +.pagination .disabled a, +.pagination .disabled a:hover { + color: #999999; + background-color: transparent; + cursor: default; +} +.pagination li:first-child a { + border-left-width: 1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.pagination li:last-child a { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.pagination-centered { + text-align: center; +} +.pagination-right { + text-align: right; +} +.pager { + margin-left: 0; + margin-bottom: 18px; + list-style: none; + text-align: center; + *zoom: 1; +} +.pager:before, +.pager:after { + display: table; + content: ""; +} +.pager:after { + clear: both; +} +.pager li { + display: inline; +} +.pager a { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} +.pager a:hover { + text-decoration: none; + background-color: #f5f5f5; +} +.pager .next a { + float: right; +} +.pager .previous a { + float: left; +} +.pager .disabled a, +.pager .disabled a:hover { + color: #999999; + background-color: #fff; + cursor: default; +} +.modal-open .dropdown-menu { + z-index: 2050; +} +.modal-open .dropdown.open { + *z-index: 2050; +} +.modal-open .popover { + z-index: 2060; +} +.modal-open .tooltip { + z-index: 2070; +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} +.modal-backdrop.fade { + opacity: 0; +} +.modal-backdrop, +.modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} +.modal { + position: fixed; + top: 50%; + left: 50%; + z-index: 1050; + overflow: auto; + width: 560px; + margin: -250px 0 0 -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + /* IE6-7 */ + + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.modal.fade { + -webkit-transition: opacity .3s linear, top .3s ease-out; + -moz-transition: opacity .3s linear, top .3s ease-out; + -ms-transition: opacity .3s linear, top .3s ease-out; + -o-transition: opacity .3s linear, top .3s ease-out; + transition: opacity .3s linear, top .3s ease-out; + top: -25%; +} +.modal.fade.in { + top: 50%; +} +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} +.modal-header .close { + margin-top: 2px; +} +.modal-body { + overflow-y: auto; + max-height: 400px; + padding: 15px; +} +.modal-form { + margin-bottom: 0; +} +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + text-align: right; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; + *zoom: 1; +} +.modal-footer:before, +.modal-footer:after { + display: table; + content: ""; +} +.modal-footer:after { + clear: both; +} +.modal-footer .btn + .btn { + margin-left: 5px; + margin-bottom: 0; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.tooltip { + position: absolute; + z-index: 1020; + display: block; + visibility: visible; + padding: 5px; + font-size: 11px; + opacity: 0; + filter: alpha(opacity=0); +} +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} +.tooltip.top { + margin-top: -2px; +} +.tooltip.right { + margin-left: 2px; +} +.tooltip.bottom { + margin-top: 2px; +} +.tooltip.left { + margin-left: -2px; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #000000; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + padding: 5px; +} +.popover.top { + margin-top: -5px; +} +.popover.right { + margin-left: 5px; +} +.popover.bottom { + margin-top: 5px; +} +.popover.left { + margin-left: -5px; +} +.popover.top .arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.popover.right .arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #000000; +} +.popover.bottom .arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; +} +.popover.left .arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} +.popover .arrow { + position: absolute; + width: 0; + height: 0; +} +.popover-inner { + padding: 3px; + width: 280px; + overflow: hidden; + background: #000000; + background: rgba(0, 0, 0, 0.8); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); +} +.popover-title { + padding: 9px 15px; + line-height: 1; + background-color: #f5f5f5; + border-bottom: 1px solid #eee; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.popover-content { + padding: 14px; + background-color: #ffffff; + -webkit-border-radius: 0 0 3px 3px; + -moz-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.popover-content p, +.popover-content ul, +.popover-content ol { + margin-bottom: 0; +} +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} +.thumbnails:before, +.thumbnails:after { + display: table; + content: ""; +} +.thumbnails:after { + clear: both; +} +.thumbnails > li { + float: left; + margin: 0 0 18px 20px; +} +.thumbnail { + display: block; + padding: 4px; + line-height: 1; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); +} +a.thumbnail:hover { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} +.thumbnail > img { + display: block; + max-width: 100%; + margin-left: auto; + margin-right: auto; +} +.thumbnail .caption { + padding: 9px; +} +.label { + padding: 1px 4px 2px; + font-size: 10.998px; + font-weight: bold; + line-height: 13px; + color: #ffffff; + vertical-align: middle; + white-space: nowrap; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #999999; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.label:hover { + color: #ffffff; + text-decoration: none; +} +.label-important { + background-color: #b94a48; +} +.label-important:hover { + background-color: #953b39; +} +.label-warning { + background-color: #f89406; +} +.label-warning:hover { + background-color: #c67605; +} +.label-success { + background-color: #468847; +} +.label-success:hover { + background-color: #356635; +} +.label-info { + background-color: #3a87ad; +} +.label-info:hover { + background-color: #2d6987; +} +.label-inverse { + background-color: #333333; +} +.label-inverse:hover { + background-color: #1a1a1a; +} +.badge { + padding: 1px 9px 2px; + font-size: 12.025px; + font-weight: bold; + white-space: nowrap; + color: #ffffff; + background-color: #999999; + -webkit-border-radius: 9px; + -moz-border-radius: 9px; + border-radius: 9px; +} +.badge:hover { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} +.badge-error { + background-color: #b94a48; +} +.badge-error:hover { + background-color: #953b39; +} +.badge-warning { + background-color: #f89406; +} +.badge-warning:hover { + background-color: #c67605; +} +.badge-success { + background-color: #468847; +} +.badge-success:hover { + background-color: #356635; +} +.badge-info { + background-color: #3a87ad; +} +.badge-info:hover { + background-color: #2d6987; +} +.badge-inverse { + background-color: #333333; +} +.badge-inverse:hover { + background-color: #1a1a1a; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +@-moz-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +@-ms-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +.progress { + overflow: hidden; + height: 18px; + margin-bottom: 18px; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -ms-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(top, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.progress .bar { + width: 0%; + height: 18px; + color: #ffffff; + font-size: 12px; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -ms-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(top, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -ms-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} +.progress-striped .bar { + background-color: #149bdf; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-danger .bar { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(top, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); +} +.progress-danger.progress-striped .bar { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-success .bar { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -ms-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(top, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0); +} +.progress-success.progress-striped .bar { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-info .bar { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(top, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0); +} +.progress-info.progress-striped .bar { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-warning .bar { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -ms-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); +} +.progress-warning.progress-striped .bar { + background-color: #fbb450; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.accordion { + margin-bottom: 18px; +} +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.accordion-heading { + border-bottom: 0; +} +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} +.carousel { + position: relative; + margin-bottom: 18px; + line-height: 1; +} +.carousel-inner { + overflow: hidden; + width: 100%; + position: relative; +} +.carousel .item { + display: none; + position: relative; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -ms-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} +.carousel .item > img { + display: block; + line-height: 1; +} +.carousel .active, +.carousel .next, +.carousel .prev { + display: block; +} +.carousel .active { + left: 0; +} +.carousel .next, +.carousel .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel .next { + left: 100%; +} +.carousel .prev { + left: -100%; +} +.carousel .next.left, +.carousel .prev.right { + left: 0; +} +.carousel .active.left { + left: -100%; +} +.carousel .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} +.carousel-control.right { + left: auto; + right: 15px; +} +.carousel-control:hover { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} +.carousel-caption { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 10px 15px 5px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} +.carousel-caption h4, +.carousel-caption p { + color: #ffffff; +} +.hero-unit { + padding: 60px; + margin-bottom: 30px; + background-color: #eeeeee; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + color: inherit; + letter-spacing: -1px; +} +.hero-unit p { + font-size: 18px; + font-weight: 200; + line-height: 27px; + color: inherit; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.hide { + display: none; +} +.show { + display: block; +} +.invisible { + visibility: hidden; +} diff --git a/data/doc-prince-book-generation/resources/exercices/exo3/site/web/index.php b/data/doc-prince-book-generation/resources/exercices/exo3/site/web/index.php new file mode 100644 index 0000000..52695aa --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo3/site/web/index.php @@ -0,0 +1,92 @@ +takeMoney($amount); + } catch (\Exception $e) { + $error = $e; + } + break; + case 'add': + default: + $account->addMoney($amount); + break; + } +} + + +// +// Process : reset the account +if (filter_has_var(INPUT_POST, 'reset')) { + $balance = filter_input(INPUT_POST, 'reset', FILTER_VALIDATE_FLOAT); + $_SESSION['account']->setBalance($balance); +} + +?> + + + + + +
+
+

Welcome

+ logout +
+ +
Vous avez getBalance(); ?> euro sur votre compte.
+ %s', $error->getMessage()); + } + ?> +
+

+ Bonjour . + Que voulez-vous faire ? +

+
+
+ + +
+
+ + +
+ +
+
+ +
+
+ Vous pouvez aussi reinitialiser votre compte : +
+
+ + +
+ +
+
+
+ + \ No newline at end of file diff --git a/data/doc-prince-book-generation/resources/exercices/exo3/site/web/login.php b/data/doc-prince-book-generation/resources/exercices/exo3/site/web/login.php new file mode 100644 index 0000000..7074af3 --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo3/site/web/login.php @@ -0,0 +1,33 @@ + + + + + + +
+
+
+ + + +
+
+
+ + \ No newline at end of file diff --git a/data/doc-prince-book-generation/resources/exercices/exo3/site/web/logout.php b/data/doc-prince-book-generation/resources/exercices/exo3/site/web/logout.php new file mode 100644 index 0000000..9554473 --- /dev/null +++ b/data/doc-prince-book-generation/resources/exercices/exo3/site/web/logout.php @@ -0,0 +1,7 @@ +addContent($data); +$request->send(); +``` +Voici la seconde: +``` +$request = new Response('http://myurl'); +``` +avec soit la création d'un context personnalisé +``` +$request->createContext('myhttp1.1method') +$request->addContent($data); +$request->send(); +``` +Soit les methodes HTTP1.1 +``` +$request->get($data); +$request->post($data); +$request->put($data); +$request->delete($data); +``` +Voici la dernière qui reste la plus facile à utiliser, +pour les développeurs qui comprennent les notations chainées: +``` +$request = new Response(); +$request->setUrl('http://myurl')->get($data) +$request->setUrl('http://myurl')->post($data) +$request->setUrl('http://myurl')->put($data) +$request->setUrl('http://myurl')->delete($data) +``` \ No newline at end of file diff --git a/data/empty.html b/data/empty.html new file mode 100644 index 0000000..7de3385 --- /dev/null +++ b/data/empty.html @@ -0,0 +1,12358 @@ + + + + + + + + + + + + + + + + + + + + +TiddlyWiki — a non-linear personal web notebook + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + diff --git a/data/jdepend-output.php b/data/jdepend-output.php new file mode 100644 index 0000000..47a4976 --- /dev/null +++ b/data/jdepend-output.php @@ -0,0 +1,15 @@ + +load(__DIR__ . DIRECTORY_SEPARATOR .'jdepend-summary.xml'); + +$xsl = new DOMDocument; +$xsl->load(__DIR__ . DIRECTORY_SEPARATOR .'jdepend-summary.xsl'); + +// Configuration du transformateur +$proc = new XSLTProcessor; +$proc->importStyleSheet($xsl); // attachement des règles xsl + +echo $proc->transformToXML($xml); diff --git a/data/jdepend-summary.xsl b/data/jdepend-summary.xsl new file mode 100644 index 0000000..93af979 --- /dev/null +++ b/data/jdepend-summary.xsl @@ -0,0 +1,99 @@ + + + + + + +

+ Project + + + cyclo: + + calls: + + + abstract: + concrete: + interfaces: + methods: + +

+ +

+ + code rank: + +

+
    + + + +
+
+ + +
+ +

+ + + position-top-10 + + + position-top-20 + + + + weighted method count: + + outgoing coupling: + +

+
    + + + +
+
+ +

+ + + position-top-10 + + + position-top-20 + + + ccn-is-low + + + + cyclo: + + npath: + +

+
+
\ No newline at end of file diff --git a/tests/behat/composer.json b/tests/behat/composer.json new file mode 100644 index 0000000..bab7f75 --- /dev/null +++ b/tests/behat/composer.json @@ -0,0 +1,25 @@ +{ + "autoload": { + "files": [ + "./application/config/app-constantes.php", + "./application/config/app-parameters.php", + "./application/config/bdd-parameters.php" + ], + "psr-4": { + "MVC\\Classe\\" : "./application/class", + "MVC\\Object\\" : "./application/objets", + "MVC\\Domain\\" : "./domain", + "MVC\\Command\\": "./console/command" + } + }, + + "require": { + "behat/behat": "^3.8", + "behat/mink": "^1.8", + "behat/mink-goutte-driver": "^1.2" + }, + + "config": { + "bin-dir": "bin/" + } +} diff --git a/tests/behat/features/bootstrap/FeatureContext.php b/tests/behat/features/bootstrap/FeatureContext.php new file mode 100644 index 0000000..c6b7bfd --- /dev/null +++ b/tests/behat/features/bootstrap/FeatureContext.php @@ -0,0 +1,92 @@ +start(); + $session->visit('http://sand-framework.dev.wm/'); + $page = $session->getPage(); + $connectedText = $page->find('xpath', '/html/body/div/div[2]/div/span[1]'); + if($connectedText !== "Vous êtes connecté en tant que $arg1") { + throw new PendingException(); + } + } + + /** + * @Given je suis sur :arg1 + */ + public function jeSuisSur($arg1) + { + $session = $this->getMink()->getSession(); + $url = $session->getCurrentUrl(); + if($url !== "http://sand-framework.dev.wm$arg1") { + throw new PendingException(); + } + } + + /** + * @When je clique sur :arg1 + */ + public function jeCliqueSur($arg1) + { + $link_tab = array( + 'Documentation' => 1, + 'Dépot' => 2, + 'Donate' => 3, + 'CGU Terms' => 4, + 'Policy' => 5 + ); + + $session = $this->getMink()->getSession(); + $page = $session->getPage(); + $page->find('xpath', "/html/body/div/div[1]/div/ul/li[{$link_tab[$arg1]}]/a")->click(); + } + + /** + * @Then je devrais voir :arg1 + */ + public function jeDevraisVoir($arg1) + { + $link_tab = array( + 'Sommaire' => '/html/body/div/section/div/h1', + 'GitList' => '/html/body/div/section/div/div[1]/div/div/a[2]', + 'Become a Sponsor !' => '//*[@id="donate"]', + 'Conditions Générale de l\'application' => '/html/body/div/section/div/div/h1', + 'Politique Générale de Sécurité' => '/html/body/div/section/div/div/h1' + ); + $session = $this->getMink()->getSession(); + $page = $session->getPage(); + + $connectedText = $page->find('xpath', $link_tab[$arg1]); + if($connectedText !== $arg1) { + throw new PendingException(); + } + } +} diff --git a/tests/behat/features/interface.feature b/tests/behat/features/interface.feature new file mode 100644 index 0000000..60276b5 --- /dev/null +++ b/tests/behat/features/interface.feature @@ -0,0 +1,29 @@ +#language: fr +Fonctionnalité: Naviguer sur l'interface + Afin de découvrir l'interface + En tant qu'utilisateur anonymous + Je peux naviguer sur l'interface + + Contexte: + Etant donné que je suis connecté en tant que "anonymous" + Et je suis sur "/" + + Scénario: Consulter la documentation + Quand je clique sur "Documentation" + Alors je devrais voir "Sommaire" + + Scénario: Consulter le dépot Git + Quand je clique sur "Dépot" + Alors je devrais voir "GitList" + + Scénario: Consulter la page donation + Quand je clique sur "Donate" + Alors je devrais voir "Become a Sponsor !" + + Scénario: Consulter les CGU + Quand je clique sur "CGU Terms" + Alors je devrais voir "Conditions Générale de l'application:" + + Scénario: Consulter la Politique de Confidentilité + Quand je clique sur "Policy" + Alors je devrais voir "Politique Générale de Sécurité" \ No newline at end of file diff --git a/tests/phpunit/composer.json b/tests/phpunit/composer.json new file mode 100644 index 0000000..b76b3de --- /dev/null +++ b/tests/phpunit/composer.json @@ -0,0 +1,23 @@ +{ + "autoload": { + "files": [ + "./application/config/app-constantes.php", + "./application/config/app-parameters.php", + "./application/config/bdd-parameters.php" + ], + "psr-4": { + "MVC\\Classe\\" : "./application/class", + "MVC\\Object\\" : "./application/objets", + "MVC\\Domain\\" : "./domain", + "MVC\\Command\\": "./console/command" + } + }, + + "require": { + "phpunit/phpunit": "^9.5" + }, + + "config": { + "bin-dir": "bin/" + } +} diff --git a/tests/phpunit.xml b/tests/phpunit/phpunit.xml similarity index 65% rename from tests/phpunit.xml rename to tests/phpunit/phpunit.xml index c0a7209..4c0af21 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit/phpunit.xml @@ -1,8 +1,7 @@ - - ./fichiertest.class.php - ./Test.php + + ./tests/UrlTest.php diff --git a/tests/Test.php b/tests/phpunit/tests/Test.php similarity index 100% rename from tests/Test.php rename to tests/phpunit/tests/Test.php diff --git a/tests/phpunit/tests/UrlTest.php b/tests/phpunit/tests/UrlTest.php new file mode 100644 index 0000000..1056c8d --- /dev/null +++ b/tests/phpunit/tests/UrlTest.php @@ -0,0 +1,24 @@ +assertTrue(true); + }else{ + $this->assertTrue(false); + } + } +} diff --git a/tests/fichiertest.class.php b/tests/phpunit/tests/fichiertest.class.php similarity index 100% rename from tests/fichiertest.class.php rename to tests/phpunit/tests/fichiertest.class.php